Permalink
Browse files

sc-server can now load any asset from the index_root as well as the u…

…rl_root. Assets loaded from the index_root will return with a header set to expire immediately (so each request will go back to the browser), while any asset loaded from url_root is returned with a 10-years expires header. This means you can super-fast cache based loading even in developer mode. Yay!
  • Loading branch information...
1 parent 1e912a9 commit c3ae463db38175d2563c5dbb716bede23386a84b Charles Jolley committed Jan 29, 2009
View
@@ -82,11 +82,16 @@ mode :debug do
# debug settings for sc-server
:serve_exceptions => true,
:reload_project => true,
+
+ # In debug mode, we want to simply compute the build number each time
+ # to ensure the latest version is always loaded.
+ :build_number => nil,
- # set default build number in debug mode since caching is not used
- # anyway.
- :build_number => 'current',
- :use_query_cache_control => true # add query strings to control caching
+ # the fast build number method uses file mtimes. It will work correctly
+ # on a single machine, but it may produce different build numbers from
+ # one machine to the next, so it should not be used in production mode
+ # builds.
+ :compute_fast_builder_numbers => true
end
View
@@ -164,7 +164,8 @@ namespace :manifest do
# Generate composite entries for each directory...
entries_by_dirname.each do |dirname, entries|
- MANIFEST.add_composite "#{dirname}.html",
+ filename = "#{dirname}.html"
+ MANIFEST.add_composite filename,
:build_task => "build:test",
:entry_type => :test,
:ext => :html,
@@ -250,10 +250,25 @@ def compute_build_number(seen=nil)
require 'digest/md5'
# No predefined build number was found, instead let's compute it!
- digests = Dir.glob(File.join(source_root, '**', '*')).map do |path|
- allowed = File.exists?(path) && !File.directory?(path)
- allowed = allowed && !target_directory?(path)
- allowed ? Digest::SHA1.hexdigest(File.read(path)) : nil
+ if config.compute_fast_build_numbers
+ # Note: this method is not good for production builds because it
+ # depends on the file mtime, which can change from one machine to
+ # the next. It is faster though, which is better for use in
+ # development mode when you need to quickly calculate the build
+ # number for an asset.
+ digests = Dir.glob(File.join(source_root, '**', '*')).map do |path|
+ File.mtime(path)
+ end
+ else
+ # This method computes the build number based on the contents of the
+ # files. It is not as fast as using an mtime, but it will remain
+ # constant from one machine to the next so it can be used when
+ # deploying across multiple build machines, etc.
+ digests = Dir.glob(File.join(source_root, '**', '*')).map do |path|
+ allowed = File.exists?(path) && !File.directory?(path)
+ allowed = allowed && !target_directory?(path)
+ allowed ? Digest::SHA1.hexdigest(File.read(path)) : nil
+ end
end
digests.compact!
@@ -61,6 +61,9 @@ module Rack
#
class Builder
+ # used to set expires header.
+ TEN_YEARS = 10 * 364 * 24 * 60 * 60
+
# When you create a new builder, pass in one or more projects you want
# the builder to monitor for changes.
def initialize(project)
@@ -85,7 +88,7 @@ def _call(env)
return not_found("No matching target") if target.nil?
# normalize url to resolve to entry & extract the language
- url, language = normalize_url(url, target)
+ url, language, cacheable = normalize_url(url, target)
return not_found("Target requires language") if language.nil?
# lookup manifest
@@ -112,9 +115,10 @@ def _call(env)
file_size = File.size(build_path)
headers = {
"Last-Modified" => File.mtime(build_path).httpdate,
+ "Etag" => File.mtime(build_path).to_i.to_s,
"Content-Type" => mime_type(build_path),
"Content-Length" => file_size.to_s,
- "Expires" => Time.now.httpdate
+ "Expires" => (cacheable ? (Time.now + TEN_YEARS) : Time.now).httpdate
}
[200, headers, File.open(build_path, 'rb')]
end
@@ -175,25 +179,52 @@ def target_for(url)
# for all the different ways you can request an index.html file and
# convert it to a canonical form
#
- # ==== Params
- # url<String>:: The URL
+ # Returns the normalized url, the language extracted from the url and
+ # a boolean indicating whether the url is considered cacheable or not.
+ # any url beginning with the target's url_root is considered cacheable
+ # and will therefore be returned with an expires <10years> header set.
+ #
+ # === Params
+ # url:: the url to normalize
+ # target:: the suspected target url
+ #
+ # === Returns
+ # [normalized url, matched language, cacheable]
#
def normalize_url(url, target)
- # Parse the URL
- matched = url.match(/^#{Regexp.escape target.index_root}(\/([^\/\.]+))?(\/([^\/\.]+))?(\/|(\/index\.html))?$/)
+ cacheable = true
+
+ # match
+ # /foo - /foo/index.html
+ # /foo/en - /foo/en/index.html
+ # /foo/en/build_number - /foo/en/build_number/index.html
+ # /foo/en/CURRENT/resource-name
+ matched = url.match(/^#{Regexp.escape target.index_root}(\/([^\/\.]+))?(\/([^\/\.]+))?(\/(.*))?$/)
unless matched.nil?
matched_language = matched[2] || target.config.preferred_language
- matched_build_number = matched[4] || target.build_number || 'current'
- url = [target.url_root,
- matched_language, matched_build_number,
- 'index.html'] * '/'
+
+ matched_build_number = matched[4]
+ if matched_build_number.blank? || matched_build_number == 'current'
+ matched_build_number = target.build_number
+ end
+
+ resource_name = matched[6]
+ resource_name = 'index.html' if resource_name.blank?
+
+ # convert to url root based
+ url = [target.url_root, matched_language, matched_build_number,
+ resource_name] * '/'
+ cacheable = false # index_root based urls are not cacheable
+
+ # otherwise, just get the language -- url_root-based urls must be
+ # fully qualified
else
matched = url.match(/^#{Regexp.escape target.url_root}\/([^\/\.]+)/)
matched_language = matched ? matched[1] : nil
end
- return [url, matched_language]
+ return [url, matched_language, cacheable]
end
# Returns the mime type. Basically this is the Rack mime mapper with
@@ -243,23 +243,23 @@ def run_task
end
end
- describe "Generates javascript-packed.js" do
-
- before do
- @entry = entry_for('javascript-packed.js')
- end
-
- it "should generate a javascript-packed.js entry" do
- @entry.should_not be_nil
- end
-
- it "should include javascript.js entries from other targets" do
- @entry.source_entries.size.should > 0
- @entry.source_entires.each do |entry|
- entry.filename.should == 'javascript.js'
- end
- end
-
- end
+ # describe "Generates javascript-packed.js" do
+ #
+ # before do
+ # @entry = entry_for('javascript-packed.js')
+ # end
+ #
+ # it "should generate a javascript-packed.js entry" do
+ # @entry.should_not be_nil
+ # end
+ #
+ # it "should include javascript.js entries from other targets" do
+ # @entry.source_entries.size.should > 0
+ # @entry.source_entires.each do |entry|
+ # entry.filename.should == 'javascript.js'
+ # end
+ # end
+ #
+ # end
end
@@ -123,7 +123,7 @@ def run_task(load_tests=true)
entry.build_task.to_s.should == "build:test"
end
end
-
+
end
it "should create a composite entry to generate a -index.json with test entries as source (excluding composite summary entries)" do
@@ -47,68 +47,108 @@ def add_dummyfile(project)
end
- it "generates a unique build number based on content if nothing is explicitly set" do
- target = @project.target_for(:sproutcore)
- target.config.build_numbers = nil #precondition
- target.config.build_number = nil #precondition
+ describe "accurate method to compute build number" do
- target.compute_build_number.should_not be_nil
- end
+ before do
+ @target = @project.target_for(:sproutcore)
+ @target.config.build_numbers = nil #precondition
+ @target.config.build_number = nil #precondition
+ @target.config.compute_fast_build_numbers = false
+ end
+
+ it "generates a unique build number based on content if nothing is explicitly set" do
+ @target.compute_build_number.should_not be_nil
+ end
- it "changes its generated build number if contents of source files change" do
- target = @project.target_for(:sproutcore)
- target.config.build_numbers = nil #precondition
- target.config.build_number = nil #precondition
+ it "changes its generated build number if contents of source files change" do
+ old_build_number = @target.compute_build_number
+
+ # write an extra file into target for testing
+ add_dummyfile(@target)
+
+ # get new build number
+ new_build_number = @target.compute_build_number
+ new_build_number.should_not eql(old_build_number)
+ end
+ end
- old_build_number = target.compute_build_number
+ describe "fast method to compute build number" do
- # write an extra file into target for testing
- add_dummyfile(target)
+ before do
+ @target = @project.target_for(:sproutcore)
+ @target.config.build_numbers = nil #precondition
+ @target.config.build_number = nil #precondition
+ @target.config.compute_fast_build_numbers = true
+ end
- # get new build number
- new_build_number = target.compute_build_number
- new_build_number.should_not eql(old_build_number)
+ it "uses faster method to compute build numbers" do
+ start = Time.now
+ 20.times { @target.compute_build_number }
+ fast_time = Time.now - start
+
+ @target.config.compute_fast_build_numbers = false
+ start = Time.now
+ 20.times { @target.compute_build_number }
+ accurate_time = Time.now - start
+
+ fast_time.should < accurate_time
+ end
+
+ it "generates a unique build number based on content if nothing is explicitly set" do
+ @target.compute_build_number.should_not be_nil
+ end
+
+ it "changes its generated build number if contents of source files change" do
+ old_build_number = @target.compute_build_number
+
+ # write an extra file into target for testing
+ add_dummyfile(@target)
+
+ # get new build number
+ new_build_number = @target.compute_build_number
+ new_build_number.should_not eql(old_build_number)
+ end
end
it "changes generated build number if build number for a required target changes" do
target = @project.target_for(:sproutcore)
target.should_not be_nil
-
+
required = target.target_for(:desktop)
required.should_not be_nil
required.config.build_numbers = nil #precondition
required.config.build_number = nil #precondition
-
+
target.config.build_numbers = nil #precondition
target.config.build_number = nil #precondition
target.required_targets.should include(required) #precondition
-
+
old_build_number = target.compute_build_number
-
+
# write an extra file into required target for testing -- changes number
add_dummyfile(required)
-
+
# get new build number
new_build_number = target.compute_build_number
new_build_number.should_not eql(old_build_number)
end
-
+
it "does not change generated build number if a nested target that is not required by target changes" do
target = @project.target_for(:sproutcore)
target.should_not be_nil
-
+
not_required = target.target_for(:mobile)
not_required.should_not be_nil
-
+
#precondition
target.expand_required_targets.should_not include(not_required)
-
+
target = @project.target_for(:sproutcore)
old_build_number = target.compute_build_number
-
+
# write an extra file into required target for testing -- changes number
add_dummyfile(not_required)
-
+
# get new build number
new_build_number = target.compute_build_number
new_build_number.should eql(old_build_number)

0 comments on commit c3ae463

Please sign in to comment.