rack-cache-smash 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|