rack-cache-smash 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +22 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +10 -0
- data/LICENSE +20 -0
- data/README.md +51 -0
- data/Rakefile +7 -0
- data/lib/rack-cache-smash.rb +42 -0
- data/lib/rack-cache-smash/version.rb +5 -0
- data/rack-cache-smash.gemspec +20 -0
- data/test/rack_cache_smash_test.rb +122 -0
- data/test/test_helper.rb +7 -0
- metadata +59 -0
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
.idea/*
|
19
|
+
*.iml
|
20
|
+
.ruby-version
|
21
|
+
vendor
|
22
|
+
*.wip.txt
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 Eliot Sykes
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# rack-cache-smash
|
2
|
+
|
3
|
+
rack-cache-smash is a Rack middleware to cache bust **every** CSS and JS asset request, intended for development environments
|
4
|
+
only and not recommended for production.
|
5
|
+
|
6
|
+
Cache busting happens by modifying all HTML responses that contain paths to JS and CSS files. These file paths are appended
|
7
|
+
with a timestamp query string parameter to force browsers to re-download the files for every single page load as they always have a unique URL.
|
8
|
+
|
9
|
+
Admittedly, this is a sledgehammer of a middleware to overcome those browser caching issues that sometimes arise during development. I don't advise
|
10
|
+
using it in production, and I doubt that you want to.
|
11
|
+
|
12
|
+
At the time of writing this, I'm using rack-cache-smash during development to overcome a [problem in iOS 7.0.2 where incomplete asset downloads seem to be cached](http://tech.vg.no/2013/10/02/ios7-bug-shows-white-page-when-getting-304-not-modified-from-server/)
|
13
|
+
and some weirdness I've seen in IE10 on Windows 8 with assets, 304 Not Modified responses, and SSL.
|
14
|
+
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
Add the rack-cache-smash gem to the development group in your application's Gemfile:
|
19
|
+
|
20
|
+
group :development do
|
21
|
+
...
|
22
|
+
gem 'rack-cache-smash'
|
23
|
+
end
|
24
|
+
|
25
|
+
And then execute:
|
26
|
+
|
27
|
+
$ bundle
|
28
|
+
|
29
|
+
For Rails apps, add this line to config/application.rb:
|
30
|
+
|
31
|
+
config.middleware.use(Rack::CacheSmash)
|
32
|
+
|
33
|
+
For Rack apps that don't use Rails, then place Rack::CacheSmash earlier in your middleware stack than the middleware(s) that generates response HTML.
|
34
|
+
|
35
|
+
## Contributing
|
36
|
+
|
37
|
+
1. Fork it
|
38
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
39
|
+
3. Run tests (`rake test`)
|
40
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
41
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
42
|
+
5. Create new Pull Request
|
43
|
+
|
44
|
+
## Releasing a new gem
|
45
|
+
|
46
|
+
1. Increment version in lib/rack-cache-smash/version.rb in line with semantic versioning
|
47
|
+
2. Update CHANGELOG.md
|
48
|
+
3. Tests pass? (`rake test`)
|
49
|
+
4. Build the gem (`rake build`)
|
50
|
+
5. Release on rubygems.org (`rake release`)
|
51
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'rack-cache-smash/version'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class CacheSmash
|
5
|
+
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
response = @app.call(env)
|
12
|
+
headers = response[1]
|
13
|
+
return response unless html_response?(headers)
|
14
|
+
|
15
|
+
status = response.first
|
16
|
+
original_body_arr = response.last
|
17
|
+
body_str = cache_bust_asset_paths_in_body(original_body_arr)
|
18
|
+
return [status, headers, [body_str]]
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
PATHS_TO_CACHE_SMASH_REGEXP = /(?<ext>\.(?:js|css))(?:(?:\?)(?<query_string>.*?))?(?<end_quote>['"])/
|
24
|
+
HTML_CONTENT_TYPE_REGEXP = /\Atext\/html.*\z/
|
25
|
+
|
26
|
+
def html_response?(headers)
|
27
|
+
headers['Content-Type'] =~ HTML_CONTENT_TYPE_REGEXP
|
28
|
+
end
|
29
|
+
|
30
|
+
def cache_bust_asset_paths_in_body(original_body_arr)
|
31
|
+
body_str = ''
|
32
|
+
original_body_arr.each { |part| body_str << part }
|
33
|
+
timestamp = Time.now.utc.to_i
|
34
|
+
body_str.gsub!(PATHS_TO_CACHE_SMASH_REGEXP) do |match|
|
35
|
+
query_string_suffix = $~[:query_string] ? "&#{$~[:query_string]}" : ''
|
36
|
+
"#{$~[:ext]}?cachesmasher=#{timestamp}#{query_string_suffix}#{$~[:end_quote]}"
|
37
|
+
end
|
38
|
+
return body_str
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'rack-cache-smash/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "rack-cache-smash"
|
8
|
+
gem.version = Rack::CacheSmash::VERSION
|
9
|
+
gem.authors = ["Eliot Sykes"]
|
10
|
+
gem.email = ["e@jetbootlabs.com"]
|
11
|
+
gem.description = %q{Rack middleware to cache bust every CSS and JS asset request}
|
12
|
+
gem.summary = %q{Rack middleware to cache bust every CSS and JS asset request}
|
13
|
+
gem.homepage = "https://github.com/eliotsykes/rack-cache-smash"
|
14
|
+
gem.license = 'MIT'
|
15
|
+
|
16
|
+
gem.files = `git ls-files`.split($/)
|
17
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
18
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
19
|
+
gem.require_paths = ["lib"]
|
20
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
class Rack::CacheSmashTest < Test::Unit::TestCase
|
4
|
+
include Rack::Test::Methods
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@body = nil
|
8
|
+
@headers = {'Content-Type' => 'text/html'}
|
9
|
+
end
|
10
|
+
|
11
|
+
def app
|
12
|
+
status = 200
|
13
|
+
rack_app = lambda { |env| [status, @headers, @body] }
|
14
|
+
Rack::CacheSmash.new(rack_app)
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_js_path_gets_cache_busted
|
18
|
+
assert_body_transform(
|
19
|
+
[
|
20
|
+
'<html><head>',
|
21
|
+
'<script src="/assets/application.js"></head><body>',
|
22
|
+
'Hello</body></html>',
|
23
|
+
],
|
24
|
+
"<html><head><script src=\"/assets/application.js?cachesmasher=[TIMESTAMP]\"></head><body>Hello</body></html>",
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_css_path_gets_cache_busted
|
29
|
+
assert_body_transform(
|
30
|
+
[
|
31
|
+
'<html><head>',
|
32
|
+
"<link href='/assets/typography.css' rel='stylesheet' type='text/css'></head><body>",
|
33
|
+
'Hello</body></html>',
|
34
|
+
],
|
35
|
+
"<html><head><link href='/assets/typography.css?cachesmasher=[TIMESTAMP]' rel='stylesheet' type='text/css'></head><body>Hello</body></html>"
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_js_path_with_query_string_gets_cache_busted
|
40
|
+
assert_body_transform(
|
41
|
+
[
|
42
|
+
'<html><head>',
|
43
|
+
'<script src="/assets/angular.js?body=1"></head><body>',
|
44
|
+
'Hello</body></html>',
|
45
|
+
],
|
46
|
+
"<html><head><script src=\"/assets/angular.js?cachesmasher=[TIMESTAMP]&body=1\"></head><body>Hello</body></html>",
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_css_path_with_query_string_gets_cache_busted
|
51
|
+
assert_body_transform(
|
52
|
+
[
|
53
|
+
'<html><head>',
|
54
|
+
"<link href='/assets/layout.css?foo=bar&hello=world' rel='stylesheet' type='text/css'></head><body>",
|
55
|
+
'Hello</body></html>',
|
56
|
+
],
|
57
|
+
"<html><head><link href='/assets/layout.css?cachesmasher=[TIMESTAMP]&foo=bar&hello=world' rel='stylesheet' type='text/css'></head><body>Hello</body></html>"
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_multiple_asset_paths_get_cache_busted
|
62
|
+
assert_body_transform(
|
63
|
+
[
|
64
|
+
'<html><head>',
|
65
|
+
"<script src='https://somewhere.tld/some/path/to/analytics.js'>",
|
66
|
+
"<link href='/assets/layout.css?foo=bar&hello=world' rel='stylesheet' type='text/css'>",
|
67
|
+
"<link href='/assets/footer.css' rel='stylesheet' type='text/css'></head><body>",
|
68
|
+
'Hello<script type="text/javascript" src="jquery.js"><script type="text/javascript" src="plugin.js?woo=hoo"></body></html>'
|
69
|
+
],
|
70
|
+
"<html><head><script src='https://somewhere.tld/some/path/to/analytics.js?cachesmasher=[TIMESTAMP]'>" +
|
71
|
+
"<link href='/assets/layout.css?cachesmasher=[TIMESTAMP]&foo=bar&hello=world' rel='stylesheet' type='text/css'>" +
|
72
|
+
"<link href='/assets/footer.css?cachesmasher=[TIMESTAMP]' rel='stylesheet' type='text/css'></head><body>" +
|
73
|
+
"Hello<script type=\"text/javascript\" src=\"jquery.js?cachesmasher=[TIMESTAMP]\">" +
|
74
|
+
"<script type=\"text/javascript\" src=\"plugin.js?cachesmasher=[TIMESTAMP]&woo=hoo\"></body></html>"
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_does_not_modify_non_html_responses
|
79
|
+
@headers['Content-Type'] = 'text/plain'
|
80
|
+
@body = [
|
81
|
+
'<html><head>',
|
82
|
+
"<link href='/assets/typography.css' rel='stylesheet' type='text/css'></head><body>",
|
83
|
+
'Hello</body></html>',
|
84
|
+
]
|
85
|
+
unmodified_body_str = @body.join('')
|
86
|
+
|
87
|
+
get '/'
|
88
|
+
|
89
|
+
assert_equal unmodified_body_str, last_response.body
|
90
|
+
assert_equal 'text/plain', last_response.headers['content-type']
|
91
|
+
end
|
92
|
+
|
93
|
+
def test_handles_html_content_type_with_charset_specified
|
94
|
+
@headers['Content-Type'] = 'text/html; charset=utf-8'
|
95
|
+
assert_body_transform(
|
96
|
+
[
|
97
|
+
'<html><head>',
|
98
|
+
'<script src="/assets/application.js"></head><body>',
|
99
|
+
'Hello</body></html>',
|
100
|
+
],
|
101
|
+
"<html><head><script src=\"/assets/application.js?cachesmasher=[TIMESTAMP]\"></head><body>Hello</body></html>",
|
102
|
+
)
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def assert_body_transform(original, result)
|
108
|
+
@body = original
|
109
|
+
|
110
|
+
Timecop.freeze do
|
111
|
+
get '/'
|
112
|
+
|
113
|
+
timestamp = Time.now.utc.to_i
|
114
|
+
assert_equal(
|
115
|
+
result.gsub('[TIMESTAMP]', timestamp.to_s),
|
116
|
+
last_response.body
|
117
|
+
)
|
118
|
+
assert_match /(text\/html)|(text\/html; charset=utf-8)/, last_response.headers['content-type']
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-cache-smash
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Eliot Sykes
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-10-10 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Rack middleware to cache bust every CSS and JS asset request
|
15
|
+
email:
|
16
|
+
- e@jetbootlabs.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- .gitignore
|
22
|
+
- CHANGELOG.md
|
23
|
+
- Gemfile
|
24
|
+
- LICENSE
|
25
|
+
- README.md
|
26
|
+
- Rakefile
|
27
|
+
- lib/rack-cache-smash.rb
|
28
|
+
- lib/rack-cache-smash/version.rb
|
29
|
+
- rack-cache-smash.gemspec
|
30
|
+
- test/rack_cache_smash_test.rb
|
31
|
+
- test/test_helper.rb
|
32
|
+
homepage: https://github.com/eliotsykes/rack-cache-smash
|
33
|
+
licenses:
|
34
|
+
- MIT
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options: []
|
37
|
+
require_paths:
|
38
|
+
- lib
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
46
|
+
none: false
|
47
|
+
requirements:
|
48
|
+
- - ! '>='
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '0'
|
51
|
+
requirements: []
|
52
|
+
rubyforge_project:
|
53
|
+
rubygems_version: 1.8.23
|
54
|
+
signing_key:
|
55
|
+
specification_version: 3
|
56
|
+
summary: Rack middleware to cache bust every CSS and JS asset request
|
57
|
+
test_files:
|
58
|
+
- test/rack_cache_smash_test.rb
|
59
|
+
- test/test_helper.rb
|