pinion 0.1.2 → 0.1.3
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/README.md +33 -18
- data/lib/pinion/asset.rb +23 -0
- data/lib/pinion/conversion.rb +18 -9
- data/lib/pinion/directory_watcher.rb +37 -0
- data/lib/pinion/server.rb +57 -71
- data/lib/pinion/version.rb +1 -1
- metadata +8 -12
data/README.md
CHANGED
@@ -15,13 +15,15 @@ simple and lightweight solution. It is driven by these core goals (bold goals ar
|
|
15
15
|
* **No added syntax to your assets (e.g. no `//= require my_other_asset`)**
|
16
16
|
* **Recompile all compiled assets when they change (or dependencies change) in development and set mtimes**
|
17
17
|
* Recompile asynchronously from requests (no polling allowed)
|
18
|
-
* Compile assets one time in production
|
18
|
+
* **Compile assets one time in production**
|
19
19
|
|
20
20
|
Installation
|
21
21
|
============
|
22
22
|
|
23
23
|
$ gem install pinion
|
24
24
|
|
25
|
+
You should add pinion to your project's Gemfile.
|
26
|
+
|
25
27
|
Usage
|
26
28
|
=====
|
27
29
|
|
@@ -32,37 +34,50 @@ The easiest way to use Pinion is to map your desired asset mount point to a `Pin
|
|
32
34
|
require "pinion"
|
33
35
|
require "your_app.rb"
|
34
36
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
37
|
+
MOUNT_POINT = "/assets"
|
38
|
+
pinion = Pinion::Server.new(MOUNT_POINT)
|
39
|
+
# Tell Pinion each type of conversion it should perform
|
40
|
+
pinion.convert :scss => :css # Sass and Coffeescript will just work if you have the gems installed
|
41
|
+
pinion.convert :coffee => :js # Conversion types correspond to file extensions. .coffee -> .js
|
42
|
+
pinion.convert :styl => :css do |file_contents|
|
43
|
+
Stylus.compile file_contents # Requires the stylus gem
|
44
|
+
end
|
45
|
+
# Tell Pinion the paths to watch
|
46
|
+
pinion.watch "public/javascripts"
|
47
|
+
pinion.watch "public/scss"
|
48
|
+
pinion.watch "public/stylus"
|
49
|
+
|
50
|
+
map MOUNT_POINT do
|
47
51
|
# Boom
|
48
|
-
run
|
52
|
+
run pinion
|
49
53
|
end
|
50
54
|
|
51
55
|
map "/" do
|
52
|
-
|
56
|
+
# You should pass pinion into your app in order to use its helper methods.
|
57
|
+
run Your::App.new(pinion)
|
53
58
|
end
|
54
59
|
```
|
55
60
|
|
61
|
+
In your app, you will use pinion's helper methods to construct urls:
|
62
|
+
|
63
|
+
``` erb
|
64
|
+
<head>
|
65
|
+
<title>My App</title>
|
66
|
+
<link type="text/css" rel="stylesheet" href="<%= pinion.asset_url("/assets/style.css") %>" />
|
67
|
+
<!-- Shorthand equivalent -->
|
68
|
+
<%= pinion.css_url("style.css") %>
|
69
|
+
</head>
|
70
|
+
```
|
71
|
+
|
56
72
|
Notes
|
57
73
|
-----
|
58
74
|
|
59
|
-
* Everything in `/assets` (in the example) will be served as-is. No conversions will be performed on those
|
60
|
-
files (unless you add `/assets` as a watch path).
|
61
75
|
* Currently, Pinion sidesteps the dependency question by invalidating its cache of each file of a particular
|
62
76
|
type (say, all `.scss` files) when any such source file is changed.
|
63
77
|
* The order that paths are added to the watch list is a priority order in case of conflicting assets. (For
|
64
78
|
intance, if `foo/bar` and `foo/baz` are both on the watch list, and both of the files `foo/bar/style.scss`
|
65
|
-
and `foo/baz/style.scss` exist, then `foo/bar/style.scss` will be used if a request occurs for
|
79
|
+
and `foo/baz/style.scss` exist, then `foo/bar/style.scss` will be used if a request occurs for
|
80
|
+
`/style.css`.)
|
66
81
|
|
67
82
|
You can see an example app using Pinion and Sinatra in the `example/` directory.
|
68
83
|
|
data/lib/pinion/asset.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require "digest/md5"
|
2
|
+
|
3
|
+
module Pinion
|
4
|
+
class Asset
|
5
|
+
attr_reader :uncompiled_path, :compiled_path, :from_type, :to_type, :compiled_contents, :length, :mtime,
|
6
|
+
:content_type, :checksum
|
7
|
+
|
8
|
+
def initialize(uncompiled_path, compiled_path, conversion, mtime)
|
9
|
+
@uncompiled_path = uncompiled_path
|
10
|
+
@compiled_path = compiled_path
|
11
|
+
@from_type = conversion.from_type
|
12
|
+
@to_type = conversion.to_type
|
13
|
+
@compiled_contents = conversion.convert(File.read(uncompiled_path))
|
14
|
+
@length = Rack::Utils.bytesize(@compiled_contents)
|
15
|
+
@mtime = mtime
|
16
|
+
@content_type = conversion.content_type
|
17
|
+
@checksum = Digest::MD5.hexdigest(@compiled_contents)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Allow the Asset to be served as a rack response body
|
21
|
+
def each() yield @compiled_contents end
|
22
|
+
end
|
23
|
+
end
|
data/lib/pinion/conversion.rb
CHANGED
@@ -22,6 +22,7 @@ module Pinion
|
|
22
22
|
attr_reader :from_type, :to_type, :gem_required
|
23
23
|
|
24
24
|
def initialize(from_type, to_type)
|
25
|
+
@loaded = false
|
25
26
|
@from_type = from_type
|
26
27
|
@to_type = to_type
|
27
28
|
@gem_required = nil
|
@@ -45,16 +46,11 @@ module Pinion
|
|
45
46
|
raise Error, "No known content-type for #{@to_type}."
|
46
47
|
end
|
47
48
|
end
|
48
|
-
def convert(file_contents)
|
49
|
-
|
50
|
-
|
51
|
-
return unless @gem_required
|
52
|
-
begin
|
53
|
-
require @gem_required
|
54
|
-
rescue LoadError => e
|
55
|
-
raise "Tried to load conversion for #{signature.inspect}, but failed to load the #{@gem_required} gem"
|
56
|
-
end
|
49
|
+
def convert(file_contents)
|
50
|
+
require_dependency
|
51
|
+
@conversion_fn.call(file_contents, @environment)
|
57
52
|
end
|
53
|
+
def add_watch_directory(path) @watch_fn.call(path, @environment) end
|
58
54
|
|
59
55
|
def verify
|
60
56
|
unless [@from_type, @to_type].all? { |s| s.is_a? Symbol }
|
@@ -64,6 +60,19 @@ module Pinion
|
|
64
60
|
raise Error, "Must provide a conversion function with convert { |file_contents| ... }."
|
65
61
|
end
|
66
62
|
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def require_dependency
|
67
|
+
return if @loaded
|
68
|
+
@loaded = true
|
69
|
+
return unless @gem_required
|
70
|
+
begin
|
71
|
+
require @gem_required
|
72
|
+
rescue LoadError => e
|
73
|
+
raise "Tried to load conversion for #{signature.inspect}, but failed to load the #{@gem_required} gem"
|
74
|
+
end
|
75
|
+
end
|
67
76
|
end
|
68
77
|
|
69
78
|
# Define built-in conversions
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "set"
|
2
|
+
require "time"
|
3
|
+
|
4
|
+
module Pinion
|
5
|
+
class DirectoryWatcher
|
6
|
+
def initialize(root = ".")
|
7
|
+
@root = root
|
8
|
+
@watch_directories = Set.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def <<(directory) @watch_directories << File.join(@root, directory) end
|
12
|
+
|
13
|
+
def glob(pattern, &block)
|
14
|
+
enumerator = Enumerator.new do |yielder|
|
15
|
+
@watch_directories.each do |directory|
|
16
|
+
Dir.glob(File.join(directory, pattern)) { |filename| yielder.yield filename }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
enumerator.each(&block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def find(path)
|
23
|
+
@watch_directories.each do |directory|
|
24
|
+
result = File.join(directory, path)
|
25
|
+
return result if File.file? result
|
26
|
+
end
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def latest_mtime_with_suffix(suffix)
|
31
|
+
pattern = "**/*#{DirectoryWatcher.sanitize_for_glob(suffix)}"
|
32
|
+
glob(pattern).reduce(Time.at(0)) { |latest, path| [latest, File.stat(path).mtime].max }
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.sanitize_for_glob(pattern) pattern.gsub(/[\*\?\[\]\{\}]/) { |match| "\\#{match}" } end
|
36
|
+
end
|
37
|
+
end
|
data/lib/pinion/server.rb
CHANGED
@@ -1,23 +1,19 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require "pinion/error"
|
1
|
+
require "pinion/asset"
|
4
2
|
require "pinion/conversion"
|
5
|
-
require "
|
6
|
-
require "
|
3
|
+
require "pinion/directory_watcher"
|
4
|
+
require "pinion/error"
|
7
5
|
|
8
6
|
module Pinion
|
9
7
|
class Server
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
@
|
16
|
-
@watch_directories = []
|
17
|
-
@watches = []
|
8
|
+
# TODO: is there a way to figure out the mount point ourselves? The only way I can find would be to wait
|
9
|
+
# for a request and compare REQUEST_PATH to PATH_INFO, but that's super hacky and won't work anyway
|
10
|
+
# because we need that information before requests are handled due to #asset_url
|
11
|
+
def initialize(mount_point)
|
12
|
+
@mount_point = mount_point
|
13
|
+
@watcher = DirectoryWatcher.new
|
18
14
|
@cached_assets = {}
|
19
|
-
@conversions_used = Set.new
|
20
15
|
@file_server = Rack::File.new(Dir.pwd)
|
16
|
+
@environment = (defined?(RACK_ENV) && RACK_ENV) || ENV["RACK_ENV"] || "development"
|
21
17
|
end
|
22
18
|
|
23
19
|
def convert(from_and_to, &block)
|
@@ -42,15 +38,13 @@ module Pinion
|
|
42
38
|
|
43
39
|
def watch(path)
|
44
40
|
raise Error, "#{path} is not a directory." unless File.directory? path
|
45
|
-
@
|
41
|
+
@watcher << path
|
46
42
|
Conversion.add_watch_directory path
|
47
43
|
end
|
48
44
|
|
49
45
|
# Boilerplate mostly stolen from sprockets
|
50
46
|
# https://github.com/sstephenson/sprockets/blob/master/lib/sprockets/server.rb
|
51
47
|
def call(env)
|
52
|
-
start unless @running
|
53
|
-
|
54
48
|
# Avoid modifying the session state, don't set cookies, etc
|
55
49
|
env["rack.session.options"] ||= {}
|
56
50
|
env["rack.session.options"].merge! :defer => true, :skip => true
|
@@ -62,7 +56,12 @@ module Pinion
|
|
62
56
|
return [403, { "Content-Type" => "text/plain", "Content-Length" => "9" }, ["Forbidden"]]
|
63
57
|
end
|
64
58
|
|
65
|
-
|
59
|
+
# Pull out the md5sum if it's part of the given path
|
60
|
+
# e.g. foo/bar-a95c53a7a0f5f492a74499e70578d150.js -> a95c53a7a0f5f492a74499e70578d150
|
61
|
+
checksum_tag = path[/-([\da-f]{32})\..+$/, 1]
|
62
|
+
path.sub!("-#{checksum_tag}", "") if checksum_tag
|
63
|
+
|
64
|
+
real_file = @watcher.find path
|
66
65
|
if real_file
|
67
66
|
# Total hack; this is probably a big misuse of Rack::File but I don't want to have to reproduce a lot
|
68
67
|
# of its logic
|
@@ -71,17 +70,23 @@ module Pinion
|
|
71
70
|
return @file_server.call(env)
|
72
71
|
end
|
73
72
|
|
74
|
-
asset =
|
73
|
+
asset = find_asset(path)
|
74
|
+
|
75
|
+
# If the ETag matches, give a 304
|
76
|
+
return [304, {}, []] if env["HTTP_IF_NONE_MATCH"] == %Q["#{asset.checksum}"]
|
77
|
+
|
75
78
|
if asset
|
79
|
+
# Cache for a year in production; don't cache in dev
|
80
|
+
cache_policy = checksum_tag ? "max-age=31536000" : "must-revalidate"
|
76
81
|
headers = {
|
77
82
|
"Content-Type" => asset.content_type,
|
78
83
|
"Content-Length" => asset.length.to_s,
|
79
|
-
|
80
|
-
"Cache-Control" => "public,
|
84
|
+
"ETag" => %Q["#{asset.checksum}"],
|
85
|
+
"Cache-Control" => "public, #{cache_policy}",
|
81
86
|
"Last-Modified" => asset.mtime.httpdate,
|
82
87
|
}
|
83
88
|
return [200, headers, []] if env["REQUEST_METHOD"] == "HEAD"
|
84
|
-
[200, headers, asset
|
89
|
+
[200, headers, asset]
|
85
90
|
else
|
86
91
|
[404, { "Content-Type" => "text/plain", "Content-Length" => "9" }, ["Not found"]]
|
87
92
|
end
|
@@ -93,28 +98,40 @@ module Pinion
|
|
93
98
|
raise
|
94
99
|
end
|
95
100
|
|
96
|
-
|
101
|
+
# Helper methods for an application to generate urls (with fingerprints in production)
|
102
|
+
def asset_url(path)
|
103
|
+
path.sub!(%r[^(#{@mount_point})?/?], "")
|
104
|
+
mounted_path = "#{@mount_point}/#{path}"
|
97
105
|
|
98
|
-
|
99
|
-
@
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
106
|
+
# TODO: Change the real file behavior if I replace the use of Rack::File above
|
107
|
+
return mounted_path if @watcher.find(path)
|
108
|
+
|
109
|
+
return mounted_path unless @environment == "production"
|
110
|
+
|
111
|
+
# Add on a checksum tag in production
|
112
|
+
asset = find_asset(path)
|
113
|
+
raise "Error: no such asset available: #{path}" unless asset
|
114
|
+
mounted_path, dot, extension = mounted_path.rpartition(".")
|
115
|
+
return mounted_path if dot.empty?
|
116
|
+
"#{mounted_path}-#{asset.checksum}.#{extension}"
|
104
117
|
end
|
118
|
+
def css_url(path) %Q{<link type="text/css" rel="stylesheet" href="#{asset_url(path)}" />} end
|
119
|
+
def js_url(path) %Q{<script type="application/javascript" src="#{asset_url(path)}"></script>} end
|
105
120
|
|
106
|
-
|
121
|
+
private
|
122
|
+
|
123
|
+
def find_asset(to_path)
|
107
124
|
asset = @cached_assets[to_path]
|
108
125
|
if asset
|
109
126
|
mtime = asset.mtime
|
110
|
-
latest =
|
127
|
+
latest = @watcher.latest_mtime_with_suffix(asset.from_type.to_s)
|
111
128
|
if latest > mtime
|
112
129
|
invalidate_all_assets_of_type(asset.from_type)
|
113
|
-
return
|
130
|
+
return find_asset(to_path)
|
114
131
|
end
|
115
132
|
else
|
116
133
|
begin
|
117
|
-
asset =
|
134
|
+
asset = find_uncached_asset(to_path)
|
118
135
|
rescue Error
|
119
136
|
return nil
|
120
137
|
end
|
@@ -123,41 +140,22 @@ module Pinion
|
|
123
140
|
asset
|
124
141
|
end
|
125
142
|
|
126
|
-
def
|
127
|
-
latest = Time.at(0)
|
128
|
-
@watch_directories.each do |directory|
|
129
|
-
Dir[File.join(directory, "**/*.#{type}")].each do |file|
|
130
|
-
mtime = File.stat(file).mtime
|
131
|
-
latest = mtime if mtime > latest
|
132
|
-
end
|
133
|
-
end
|
134
|
-
latest
|
135
|
-
end
|
136
|
-
|
137
|
-
def get_uncached_asset(to_path)
|
143
|
+
def find_uncached_asset(to_path)
|
138
144
|
from_path, conversion = find_source_file_and_conversion(to_path)
|
139
145
|
# If we reach this point we've found the asset we're going to compile
|
140
|
-
conversion.require_dependency unless @conversions_used.include? conversion
|
141
|
-
@conversions_used << conversion
|
142
146
|
# TODO: log at info: compiling asset ...
|
143
|
-
|
144
|
-
|
145
|
-
mtime = latest_mtime_of_type(conversion.from_type)
|
146
|
-
content_type = conversion.content_type
|
147
|
-
return Asset.new from_path, to_path, conversion.from_type, conversion.to_type,
|
148
|
-
[contents], contents.length, mtime, content_type
|
147
|
+
mtime = @watcher.latest_mtime_with_suffix(conversion.to_type.to_s)
|
148
|
+
Asset.new from_path, to_path, conversion, mtime
|
149
149
|
end
|
150
150
|
|
151
151
|
def find_source_file_and_conversion(to_path)
|
152
152
|
path, dot, suffix = to_path.rpartition(".")
|
153
153
|
conversions = Conversion.conversions_for(suffix.to_sym)
|
154
154
|
raise Error, "No conversion for for #{to_path}" if conversions.empty?
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
end
|
160
|
-
end
|
155
|
+
conversions.each do |conversion|
|
156
|
+
filename = "#{path}.#{conversion.from_type}"
|
157
|
+
from_path = @watcher.find filename
|
158
|
+
return [from_path, conversion] if from_path
|
161
159
|
end
|
162
160
|
raise Error, "No source file found for #{to_path}"
|
163
161
|
end
|
@@ -170,17 +168,5 @@ module Pinion
|
|
170
168
|
status, headers, body = response
|
171
169
|
[status, headers.merge({ "Content-Length" => Rack::Utils.bytesize(body).to_s }), body]
|
172
170
|
end
|
173
|
-
|
174
|
-
def update_asset(asset)
|
175
|
-
end
|
176
|
-
|
177
|
-
def start
|
178
|
-
@running = true
|
179
|
-
# TODO: mad threadz
|
180
|
-
# Start a thread with an FSSM watch on each directory. Upon detecting a change to a compiled file that
|
181
|
-
# is a dependency of any asset in @required_assets, call update_asset for each affected asset.
|
182
|
-
#
|
183
|
-
# There are some tricky threading issues here.
|
184
|
-
end
|
185
171
|
end
|
186
172
|
end
|
data/lib/pinion/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pinion
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-04-27 00:00:00.000000000Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rack
|
16
|
-
requirement: &
|
16
|
+
requirement: &23433260 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '1.0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *23433260
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: rake
|
27
|
-
requirement: &
|
27
|
+
requirement: &23432840 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,7 +32,7 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *23432840
|
36
36
|
description: ! 'Pinion is a Rack application that you can use to compile and serve
|
37
37
|
assets (such as Javascript and CSS).
|
38
38
|
|
@@ -44,9 +44,11 @@ extensions: []
|
|
44
44
|
extra_rdoc_files: []
|
45
45
|
files:
|
46
46
|
- README.md
|
47
|
+
- lib/pinion/asset.rb
|
47
48
|
- lib/pinion/conversion.rb
|
48
49
|
- lib/pinion/error.rb
|
49
50
|
- lib/pinion/server.rb
|
51
|
+
- lib/pinion/directory_watcher.rb
|
50
52
|
- lib/pinion/version.rb
|
51
53
|
- lib/pinion.rb
|
52
54
|
homepage: https://github.com/ooyala/pinion
|
@@ -61,18 +63,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
61
63
|
- - ! '>='
|
62
64
|
- !ruby/object:Gem::Version
|
63
65
|
version: '0'
|
64
|
-
segments:
|
65
|
-
- 0
|
66
|
-
hash: -3405067065911020567
|
67
66
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
67
|
none: false
|
69
68
|
requirements:
|
70
69
|
- - ! '>='
|
71
70
|
- !ruby/object:Gem::Version
|
72
71
|
version: '0'
|
73
|
-
segments:
|
74
|
-
- 0
|
75
|
-
hash: -3405067065911020567
|
76
72
|
requirements: []
|
77
73
|
rubyforge_project: pinion
|
78
74
|
rubygems_version: 1.8.10
|