pinion 0.1.6 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +63 -17
- data/lib/pinion/asset.rb +108 -15
- data/lib/pinion/bundle.rb +48 -0
- data/lib/pinion/bundle_type.rb +47 -0
- data/lib/pinion/compiled_asset.rb +41 -0
- data/lib/pinion/conversion.rb +15 -22
- data/lib/pinion/server.rb +41 -67
- data/lib/pinion/static_asset.rb +28 -0
- data/lib/pinion/version.rb +1 -1
- metadata +69 -10
- data/lib/pinion/directory_watcher.rb +0 -47
data/README.md
CHANGED
@@ -1,12 +1,10 @@
|
|
1
|
-
Pinion
|
2
|
-
======
|
1
|
+
# Pinion
|
3
2
|
|
4
3
|
Pinion is a Rack application that serves assets, possibly transforming them in the process. It is generally
|
5
4
|
useful for serving Javascript and CSS, and things that compile to Javascript and CSS such as Coffeescript and
|
6
5
|
Sass.
|
7
6
|
|
8
|
-
Goals
|
9
|
-
=====
|
7
|
+
# Goals
|
10
8
|
|
11
9
|
There are a lot of tools that accomplish very similar things in this space. Pinion is meant to be a very
|
12
10
|
simple and lightweight solution. It is driven by these core goals (bold goals are implemented):
|
@@ -17,15 +15,13 @@ simple and lightweight solution. It is driven by these core goals (bold goals ar
|
|
17
15
|
* Recompile asynchronously from requests (no polling allowed)
|
18
16
|
* **Compile assets one time in production**
|
19
17
|
|
20
|
-
Installation
|
21
|
-
============
|
18
|
+
# Installation
|
22
19
|
|
23
20
|
$ gem install pinion
|
24
21
|
|
25
22
|
You should add pinion to your project's Gemfile.
|
26
23
|
|
27
|
-
Usage
|
28
|
-
=====
|
24
|
+
# Usage
|
29
25
|
|
30
26
|
The easiest way to use Pinion is to map your desired asset mount point to a `Pinion::Server` instance in your
|
31
27
|
`config.ru`.
|
@@ -64,13 +60,57 @@ In your app, you will use pinion's helper methods to construct urls:
|
|
64
60
|
<head>
|
65
61
|
<title>My App</title>
|
66
62
|
<link type="text/css" rel="stylesheet" href="<%= pinion.asset_url("/assets/style.css") %>" />
|
67
|
-
|
63
|
+
<%# Shorthand equivalent %>
|
68
64
|
<%= pinion.css_url("style.css") %>
|
69
65
|
</head>
|
70
66
|
```
|
71
67
|
|
72
|
-
|
73
|
-
|
68
|
+
# Production usage
|
69
|
+
|
70
|
+
In production, you may wish to concatenate and minify your assets before you serve them. This is done through
|
71
|
+
using asset bundles. Pinion provides a predefined bundle type, `:concatenate_and_uglify_js`, for your
|
72
|
+
convenience.
|
73
|
+
|
74
|
+
You can bundle files by putting this in your app:
|
75
|
+
|
76
|
+
``` erb
|
77
|
+
<%= @pinion.js_bundle(:concatenate_and_uglify_js, "main-bundle",
|
78
|
+
"app.js",
|
79
|
+
"helpers.js",
|
80
|
+
"util.js",
|
81
|
+
"jquery.js"
|
82
|
+
) %>
|
83
|
+
```
|
84
|
+
|
85
|
+
In development, the individual `<script>` tags for each asset will be emitted; in production, a single asset
|
86
|
+
(`main-bundle.js`) will be produced.
|
87
|
+
|
88
|
+
The `:concatenate_and_uglify_js` bundle type simply concatenates JS files and runs them through
|
89
|
+
[Uglifier](https://github.com/lautis/uglifier). No default CSS bundle type is provided (but the built-in Sass
|
90
|
+
conversion type emits minified code in production, and typically you'll let Sass/Less/Stylus handle
|
91
|
+
concatenation for you).
|
92
|
+
|
93
|
+
You can define your own bundle types and their behavior if you like:
|
94
|
+
|
95
|
+
``` ruby
|
96
|
+
# The block is passed an array of `Pinion::Asset`s; it should return the content of the bundled files.
|
97
|
+
Pinion::BundleType.create(:concatenate_js_only) do |assets|
|
98
|
+
# Demo code only; you need to be a bit more careful in reality. See the definition of
|
99
|
+
# :concatenate_and_uglify_js for hints.
|
100
|
+
assets.map(&:contents).join("\n")
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
Note that in production mode, asset URLs will have the md5sum of the asset inserted into them:
|
105
|
+
|
106
|
+
``` html
|
107
|
+
<link type="text/css" rel="stylesheet" href="/assets/style-698f462d2f43890597ae78df8286d03f.css" />
|
108
|
+
<script src="/assets/test-bundle-cd94852076ffa13c006cf575dfff9e35.js"></script>
|
109
|
+
```
|
110
|
+
|
111
|
+
and these assets are served with long (1-year) expiry, for good cacheability.
|
112
|
+
|
113
|
+
# Notes
|
74
114
|
|
75
115
|
* Currently, Pinion sidesteps the dependency question by invalidating its cache of each file of a particular
|
76
116
|
type (say, all `.scss` files) when any such source file is changed.
|
@@ -79,15 +119,21 @@ Notes
|
|
79
119
|
and `foo/baz/style.scss` exist, then `foo/bar/style.scss` will be used if a request occurs for
|
80
120
|
`/style.css`.)
|
81
121
|
|
82
|
-
You can see an example app using Pinion and Sinatra in the `example/` directory.
|
122
|
+
You can see an example app using Pinion and Sinatra in the `example/` directory. Run `bundle install` in that
|
123
|
+
directory to get the necessary gems, then run it:
|
124
|
+
|
125
|
+
rackup config.ru # Development mode
|
126
|
+
RACK_ENV=production rackup config.ru # Production mode
|
83
127
|
|
84
|
-
Authors
|
85
|
-
=======
|
128
|
+
# Authors
|
86
129
|
|
87
130
|
Pinion was written by Caleb Spare ([cespare](https://github.com/cespare)). Inspiration from
|
88
|
-
[sprockets](https://github.com/sstephenson/sprockets)
|
131
|
+
[sprockets](https://github.com/sstephenson/sprockets).
|
132
|
+
|
133
|
+
Contributions from:
|
134
|
+
|
135
|
+
* Alek Storm ([alekstorm](https://github.com/alekstorm))
|
89
136
|
|
90
|
-
License
|
91
|
-
=======
|
137
|
+
# License
|
92
138
|
|
93
139
|
Pinion is released under [the MIT License](http://www.opensource.org/licenses/mit-license.php).
|
data/lib/pinion/asset.rb
CHANGED
@@ -1,23 +1,116 @@
|
|
1
|
-
require "
|
1
|
+
require "rack/mime"
|
2
|
+
require "set"
|
3
|
+
require "time"
|
4
|
+
|
5
|
+
require "pinion/conversion"
|
2
6
|
|
3
7
|
module Pinion
|
8
|
+
def self.environment() (defined?(RACK_ENV) && RACK_ENV) || ENV["RACK_ENV"] || "development" end
|
9
|
+
|
4
10
|
class Asset
|
5
|
-
|
6
|
-
|
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)
|
11
|
+
class << self
|
12
|
+
attr_reader :watch_directories, :cached_assets
|
18
13
|
end
|
19
14
|
|
15
|
+
# Paths to consider for requested assets
|
16
|
+
@watch_directories = Set.new
|
17
|
+
# Cache of raw files with locations -- used in production mode
|
18
|
+
@cached_files = {}
|
19
|
+
# Asset cache for quick lookup
|
20
|
+
@cached_assets = {}
|
21
|
+
|
22
|
+
#
|
23
|
+
# Asset methods
|
24
|
+
#
|
25
|
+
|
26
|
+
attr_reader :extension, :length, :mtime, :checksum
|
27
|
+
|
28
|
+
def initialize() raise "subclass me" end
|
29
|
+
def contents() raise "Implement me" end
|
30
|
+
|
31
|
+
# The latest mtime of this asset. For compiled assets, this the latest mtime of all files of this type.
|
32
|
+
def latest_mtime() raise "Implement me" end
|
33
|
+
|
34
|
+
# Invalidate this asset (and possibly others it depends on)
|
35
|
+
def invalidate() raise "Implement me" end
|
36
|
+
|
20
37
|
# Allow the Asset to be served as a rack response body
|
21
|
-
def each() yield
|
38
|
+
def each() yield contents end
|
39
|
+
|
40
|
+
def content_type() Rack::Mime::MIME_TYPES[".#{@extension}"] || "application/octet-stream" end
|
41
|
+
|
42
|
+
# In production mode, assume that the files won't change on the filesystem. This means we can always serve
|
43
|
+
# them from cache (if cached).
|
44
|
+
def self.static() Pinion.environment == "production" end
|
45
|
+
|
46
|
+
#
|
47
|
+
# File watcher class methods
|
48
|
+
#
|
49
|
+
|
50
|
+
# Add a path to the set of asset paths.
|
51
|
+
def self.watch_path(path) @watch_directories << File.join(".", path) end
|
52
|
+
|
53
|
+
# Find a particular file in the watched directories.
|
54
|
+
def self.find_file(path)
|
55
|
+
return @cached_files[path] if (static && @cached_files.include?(path))
|
56
|
+
result = nil
|
57
|
+
@watch_directories.each do |directory|
|
58
|
+
filename = File.join(directory, path)
|
59
|
+
if File.file? filename
|
60
|
+
result = filename
|
61
|
+
break
|
62
|
+
end
|
63
|
+
end
|
64
|
+
@cached_files[path] = result if static
|
65
|
+
result
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# Asset search methods
|
70
|
+
#
|
71
|
+
|
72
|
+
# Look up an asset by its path. It may be returned from cache.
|
73
|
+
def self.[](to_path)
|
74
|
+
asset = @cached_assets[to_path]
|
75
|
+
if asset
|
76
|
+
return asset if static
|
77
|
+
mtime = asset.mtime
|
78
|
+
latest = asset.latest_mtime
|
79
|
+
if latest > mtime
|
80
|
+
asset.invalidate
|
81
|
+
return self[to_path]
|
82
|
+
end
|
83
|
+
else
|
84
|
+
begin
|
85
|
+
asset = find_uncached_asset(to_path)
|
86
|
+
rescue Error => error
|
87
|
+
STDERR.puts "Warning: #{error.message}"
|
88
|
+
return nil
|
89
|
+
end
|
90
|
+
@cached_assets[to_path] = asset
|
91
|
+
end
|
92
|
+
asset
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.find_uncached_asset(to_path)
|
96
|
+
real_file = find_file(to_path)
|
97
|
+
return StaticAsset.new(to_path, real_file) if real_file
|
98
|
+
from_path, conversion = find_source_file_and_conversion(to_path)
|
99
|
+
# If we reach this point we've found the asset we're going to compile
|
100
|
+
# TODO: log at info: compiling asset ...
|
101
|
+
CompiledAsset.new from_path, conversion
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.find_source_file_and_conversion(to_path)
|
105
|
+
path, dot, suffix = to_path.rpartition(".")
|
106
|
+
conversions = Conversion.conversions_for(suffix.to_sym)
|
107
|
+
raise Error, "No conversion for for #{to_path}" if conversions.empty?
|
108
|
+
conversions.each do |conversion|
|
109
|
+
filename = "#{path}.#{conversion.from_type}"
|
110
|
+
from_path = find_file(filename)
|
111
|
+
return [from_path, conversion] if from_path
|
112
|
+
end
|
113
|
+
raise Error, "No source file found for #{to_path}"
|
114
|
+
end
|
22
115
|
end
|
23
116
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require "pinion/error"
|
2
|
+
require "digest/md5"
|
3
|
+
|
4
|
+
require "pinion/asset"
|
5
|
+
require "pinion/bundle_type"
|
6
|
+
|
7
|
+
module Pinion
|
8
|
+
# A `Bundle` is a set of assets of the same type that will be served as a single grouped asset in
|
9
|
+
# production. A `Bundle` has a `BundleType` that defines how to process the bundle.
|
10
|
+
class Bundle < Asset
|
11
|
+
@@bundles = {}
|
12
|
+
|
13
|
+
attr_reader :contents, :name
|
14
|
+
|
15
|
+
# Create a new `Bundle`.
|
16
|
+
def initialize(bundle_type, name, assets)
|
17
|
+
@bundle_type = bundle_type
|
18
|
+
@name = name
|
19
|
+
@assets = assets
|
20
|
+
raise Error, "No assets provided" if assets.empty?
|
21
|
+
@extension = assets.first.extension
|
22
|
+
unless assets.all? { |asset| asset.extension == @extension }
|
23
|
+
raise Error, "All assets in a bundle must have the same extension"
|
24
|
+
end
|
25
|
+
@contents = bundle_type.process(assets)
|
26
|
+
@checksum = Digest::MD5.hexdigest(@contents)
|
27
|
+
@mtime = assets.map(&:mtime).max
|
28
|
+
@length = Rack::Utils.bytesize(@contents)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Create a new bundle from a bundle_type name (e.g. `:concatenate_and_uglify_js`) and an array of
|
32
|
+
# `Asset`s. The name is taken as the identifier in the resulting path.
|
33
|
+
def self.create(bundle_type_name, name, assets)
|
34
|
+
bundle_type = BundleType[bundle_type_name]
|
35
|
+
raise Error, "No such bundle type #{bundle_type_name}" unless bundle_type
|
36
|
+
bundle = Bundle.new(bundle_type, name, assets)
|
37
|
+
@@bundles[bundle.checksum] = bundle
|
38
|
+
bundle
|
39
|
+
end
|
40
|
+
|
41
|
+
# Find a `Bundle` by its name and checksum
|
42
|
+
def self.[](name, checksum)
|
43
|
+
return nil unless name && checksum
|
44
|
+
bundle = @@bundles[checksum]
|
45
|
+
(bundle && (bundle.name == name)) ? bundle : nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Pinion
|
2
|
+
# A `BundleType` is a description of how to bundle together multiple assets of the same type. New types of
|
3
|
+
# `Bundle`s may be created with `BundleType.create`. A particular bundle type is simply a Proc that knows
|
4
|
+
# how to bundle together a set of assets. For convenience, there is a built-in `BundleType` type already
|
5
|
+
# defined, `:concatenate_and_uglify_js`.
|
6
|
+
class BundleType
|
7
|
+
@@bundle_types = {}
|
8
|
+
|
9
|
+
def initialize(definition_proc)
|
10
|
+
@definition_proc = definition_proc
|
11
|
+
end
|
12
|
+
|
13
|
+
# Process an array of `Asset`s to produce the bundled result.
|
14
|
+
def process(assets)
|
15
|
+
@definition_proc.call(assets)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Create a new bundle definition. The block will be called with argument `assets`, the array of `Asset`s.
|
19
|
+
# - assets: an array of `Asset`s
|
20
|
+
def self.create(name, &block)
|
21
|
+
@@bundle_types[name] = BundleType.new(block)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Retrieve a `BundleType` by name.
|
25
|
+
def self.[](name) @@bundle_types[name] end
|
26
|
+
end
|
27
|
+
|
28
|
+
BundleType.create(:concatenate_and_uglify_js) do |assets|
|
29
|
+
begin
|
30
|
+
require "uglifier"
|
31
|
+
rescue LoadError => e
|
32
|
+
raise "The uglifier gem is required to use the :concatenate_and_uglify_js bundle."
|
33
|
+
end
|
34
|
+
# Concatenate the contents of the assets (possibly compiling along the way)
|
35
|
+
concatenated_contents = assets.reduce("") do |concatenated_text, asset|
|
36
|
+
contents = asset.contents
|
37
|
+
# Taken from Sprockets's SafetyColons -- if the JS file is not blank and does not end in a semicolon,
|
38
|
+
# append a semicolon and newline for safety.
|
39
|
+
unless contents =~ /\A\s*\Z/m || contents =~ /;\s*\Z/m
|
40
|
+
contents << ";\n"
|
41
|
+
end
|
42
|
+
concatenated_text << contents
|
43
|
+
end
|
44
|
+
concatenated_contents
|
45
|
+
Uglifier.compile(concatenated_contents)
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "digest/md5"
|
2
|
+
|
3
|
+
require "pinion/asset"
|
4
|
+
|
5
|
+
module Pinion
|
6
|
+
class CompiledAsset < Asset
|
7
|
+
attr_reader :from_type
|
8
|
+
|
9
|
+
def initialize(uncompiled_path, conversion)
|
10
|
+
@from_type = conversion.from_type
|
11
|
+
@to_type = conversion.to_type
|
12
|
+
@compiled_contents = conversion.convert(File.read(uncompiled_path))
|
13
|
+
@length = Rack::Utils.bytesize(@compiled_contents)
|
14
|
+
@mtime = latest_mtime
|
15
|
+
@extension = @to_type.to_s
|
16
|
+
@checksum = Digest::MD5.hexdigest(@compiled_contents)
|
17
|
+
end
|
18
|
+
|
19
|
+
def contents() @compiled_contents end
|
20
|
+
|
21
|
+
def latest_mtime
|
22
|
+
pattern = "**/*#{self.class.sanitize_for_glob(".#{@from_type}")}"
|
23
|
+
self.class.glob(pattern).reduce(Time.at(0)) { |latest, path| [latest, File.stat(path).mtime].max }
|
24
|
+
end
|
25
|
+
|
26
|
+
def invalidate
|
27
|
+
Asset.cached_assets.delete_if { |_, asset| asset.is_a?(CompiledAsset) && asset.from_type == @from_type }
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.sanitize_for_glob(pattern) pattern.gsub(/[\*\?\[\]\{\}]/) { |match| "\\#{match}" } end
|
31
|
+
|
32
|
+
def self.glob(pattern, &block)
|
33
|
+
enumerator = Enumerator.new do |yielder|
|
34
|
+
Asset.watch_directories.each do |directory|
|
35
|
+
Dir.glob(File.join(directory, pattern)) { |filename| yielder.yield filename }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
enumerator.each(&block)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/pinion/conversion.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
require "pinion/error"
|
2
1
|
require "set"
|
3
2
|
|
3
|
+
require "pinion/error"
|
4
|
+
|
4
5
|
module Pinion
|
5
6
|
# A conversion describes how to convert certain types of files and create asset links for them.
|
6
7
|
# Conversions.create() provides a tiny DSL for defining new conversions
|
@@ -28,7 +29,7 @@ module Pinion
|
|
28
29
|
@gem_required = nil
|
29
30
|
@conversion_fn = nil
|
30
31
|
@watch_fn = Proc.new {} # Don't do anything by default
|
31
|
-
@
|
32
|
+
@context = {}
|
32
33
|
end
|
33
34
|
|
34
35
|
# DSL methods
|
@@ -38,19 +39,11 @@ module Pinion
|
|
38
39
|
|
39
40
|
# Instance methods
|
40
41
|
def signature() { @from_type => @to_type } end
|
41
|
-
def content_type
|
42
|
-
case @to_type
|
43
|
-
when :css then "text/css"
|
44
|
-
when :js then "application/javascript"
|
45
|
-
else
|
46
|
-
raise Error, "No known content-type for #{@to_type}."
|
47
|
-
end
|
48
|
-
end
|
49
42
|
def convert(file_contents)
|
50
43
|
require_dependency
|
51
|
-
@conversion_fn.call(file_contents, @
|
44
|
+
@conversion_fn.call(file_contents, @context)
|
52
45
|
end
|
53
|
-
def add_watch_directory(path) @watch_fn.call(path, @
|
46
|
+
def add_watch_directory(path) @watch_fn.call(path, @context) end
|
54
47
|
|
55
48
|
def verify
|
56
49
|
unless [@from_type, @to_type].all? { |s| s.is_a? Symbol }
|
@@ -78,25 +71,25 @@ module Pinion
|
|
78
71
|
# Define built-in conversions
|
79
72
|
Conversion.create :scss => :css do
|
80
73
|
require_gem "sass"
|
81
|
-
render do |file_contents,
|
82
|
-
load_paths =
|
74
|
+
render do |file_contents, context|
|
75
|
+
load_paths = context[:load_paths].to_a || []
|
83
76
|
Sass::Engine.new(file_contents, :syntax => :scss, :load_paths => load_paths).render
|
84
77
|
end
|
85
|
-
watch do |path,
|
86
|
-
|
87
|
-
|
78
|
+
watch do |path, context|
|
79
|
+
context[:load_paths] ||= Set.new
|
80
|
+
context[:load_paths] << path
|
88
81
|
end
|
89
82
|
end
|
90
83
|
|
91
84
|
Conversion.create :sass => :css do
|
92
85
|
require_gem "sass"
|
93
|
-
render do |file_contents,
|
94
|
-
load_paths =
|
86
|
+
render do |file_contents, context|
|
87
|
+
load_paths = context[:load_paths].to_a || []
|
95
88
|
Sass::Engine.new(file_contents, :syntax => :sass, :load_paths => load_paths).render
|
96
89
|
end
|
97
|
-
watch do |path,
|
98
|
-
|
99
|
-
|
90
|
+
watch do |path, context|
|
91
|
+
context[:load_paths] ||= Set.new
|
92
|
+
context[:load_paths] << path
|
100
93
|
end
|
101
94
|
end
|
102
95
|
|
data/lib/pinion/server.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
require "pinion/asset"
|
2
|
+
require "pinion/bundle"
|
3
|
+
require "pinion/compiled_asset"
|
4
|
+
require "pinion/static_asset"
|
2
5
|
require "pinion/conversion"
|
3
|
-
require "pinion/directory_watcher"
|
4
6
|
require "pinion/error"
|
5
7
|
|
6
8
|
module Pinion
|
@@ -10,9 +12,6 @@ module Pinion
|
|
10
12
|
# because we need that information before requests are handled due to #asset_url
|
11
13
|
def initialize(mount_point)
|
12
14
|
@mount_point = mount_point
|
13
|
-
@environment = (defined?(RACK_ENV) && RACK_ENV) || ENV["RACK_ENV"] || "development"
|
14
|
-
@watcher = DirectoryWatcher.new ".", :static => (@environment == "production")
|
15
|
-
@cached_assets = {}
|
16
15
|
@file_server = Rack::File.new(Dir.pwd)
|
17
16
|
end
|
18
17
|
|
@@ -38,7 +37,7 @@ module Pinion
|
|
38
37
|
|
39
38
|
def watch(path)
|
40
39
|
raise Error, "#{path} is not a directory." unless File.directory? path
|
41
|
-
|
40
|
+
Asset.watch_path(path)
|
42
41
|
Conversion.add_watch_directory path
|
43
42
|
end
|
44
43
|
|
@@ -58,19 +57,15 @@ module Pinion
|
|
58
57
|
|
59
58
|
# Pull out the md5sum if it's part of the given path
|
60
59
|
# e.g. foo/bar-a95c53a7a0f5f492a74499e70578d150.js -> a95c53a7a0f5f492a74499e70578d150
|
61
|
-
checksum_tag =
|
62
|
-
path.
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
# of its logic
|
68
|
-
# TODO: Fix this
|
69
|
-
env["PATH_INFO"] = real_file
|
70
|
-
return @file_server.call(env)
|
60
|
+
checksum_tag = bundle_name = nil
|
61
|
+
matches = path.match /([^\/]+)-([\da-f]{32})\..+$/
|
62
|
+
if matches && matches.size == 3
|
63
|
+
bundle_name = matches[1]
|
64
|
+
checksum_tag = matches[2]
|
65
|
+
path.sub!("-#{checksum_tag}", "")
|
71
66
|
end
|
72
67
|
|
73
|
-
asset =
|
68
|
+
asset = Bundle[bundle_name, checksum_tag] || Asset[path]
|
74
69
|
|
75
70
|
if asset
|
76
71
|
# If the ETag matches, give a 304
|
@@ -85,8 +80,8 @@ module Pinion
|
|
85
80
|
"Cache-Control" => "public, #{cache_policy}",
|
86
81
|
"Last-Modified" => asset.mtime.httpdate,
|
87
82
|
}
|
88
|
-
|
89
|
-
[200, headers,
|
83
|
+
body = env["REQUEST_METHOD"] == "HEAD" ? [] : asset
|
84
|
+
[200, headers, body]
|
90
85
|
else
|
91
86
|
[404, { "Content-Type" => "text/plain", "Content-Length" => "9" }, ["Not found"]]
|
92
87
|
end
|
@@ -103,67 +98,46 @@ module Pinion
|
|
103
98
|
path.sub!(%r[^(#{@mount_point})?/?], "")
|
104
99
|
mounted_path = "#{@mount_point}/#{path}"
|
105
100
|
|
106
|
-
|
107
|
-
return mounted_path if @watcher.find(path)
|
108
|
-
|
109
|
-
return mounted_path unless @environment == "production"
|
101
|
+
return mounted_path unless Pinion.environment == "production"
|
110
102
|
|
111
103
|
# Add on a checksum tag in production
|
112
|
-
asset =
|
104
|
+
asset = Asset[path]
|
113
105
|
raise "Error: no such asset available: #{path}" unless asset
|
114
106
|
mounted_path, dot, extension = mounted_path.rpartition(".")
|
115
107
|
return mounted_path if dot.empty?
|
116
108
|
"#{mounted_path}-#{asset.checksum}.#{extension}"
|
117
109
|
end
|
118
|
-
def css_url(path)
|
119
|
-
def js_url(path)
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
def
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
begin
|
135
|
-
asset = find_uncached_asset(to_path)
|
136
|
-
rescue Error
|
137
|
-
return nil
|
138
|
-
end
|
139
|
-
@cached_assets[to_path] = asset
|
110
|
+
def css_url(path) css_wrapper(asset_url(path)) end
|
111
|
+
def js_url(path) js_wrapper(asset_url(path)) end
|
112
|
+
|
113
|
+
def asset_inline(path) Asset[path].contents end
|
114
|
+
def css_inline(path) %Q{<style type="text/css">#{asset_inline(path)}</style>} end
|
115
|
+
def js_inline(path) %Q{<script>#{asset_inline(path)}</script>} end
|
116
|
+
|
117
|
+
# Bundle several assets together. In production, the single bundled result is produced; otherwise, each
|
118
|
+
# individual asset_url is returned.
|
119
|
+
def bundle_url(bundle_name, name, *paths)
|
120
|
+
return paths.map { |p| asset_url(p) } unless Pinion.environment == "production"
|
121
|
+
paths.each { |path| path.sub!(%r[^(#{@mount_point})?/?], "") }
|
122
|
+
assets = paths.map do |path|
|
123
|
+
asset = Asset[path]
|
124
|
+
raise "Error: no such asset available: #{path}" unless asset
|
125
|
+
asset
|
140
126
|
end
|
141
|
-
|
127
|
+
bundle = Bundle.create(bundle_name, name, assets)
|
128
|
+
["#{@mount_point}/#{bundle.name}-#{bundle.checksum}.#{bundle.extension}"]
|
142
129
|
end
|
143
|
-
|
144
|
-
|
145
|
-
from_path, conversion = find_source_file_and_conversion(to_path)
|
146
|
-
# If we reach this point we've found the asset we're going to compile
|
147
|
-
# TODO: log at info: compiling asset ...
|
148
|
-
mtime = @watcher.latest_mtime_with_suffix(conversion.to_type.to_s)
|
149
|
-
Asset.new from_path, to_path, conversion, mtime
|
130
|
+
def js_bundle(bundle_name, name, *paths)
|
131
|
+
bundle_url(bundle_name, name, *paths).map { |path| js_wrapper(path) }.join
|
150
132
|
end
|
151
|
-
|
152
|
-
|
153
|
-
path, dot, suffix = to_path.rpartition(".")
|
154
|
-
conversions = Conversion.conversions_for(suffix.to_sym)
|
155
|
-
raise Error, "No conversion for for #{to_path}" if conversions.empty?
|
156
|
-
conversions.each do |conversion|
|
157
|
-
filename = "#{path}.#{conversion.from_type}"
|
158
|
-
from_path = @watcher.find filename
|
159
|
-
return [from_path, conversion] if from_path
|
160
|
-
end
|
161
|
-
raise Error, "No source file found for #{to_path}"
|
133
|
+
def css_bundle(bundle_name, name, *paths)
|
134
|
+
bundle_url(bundle_name, name, *paths).map { |path| css_wrapper(path) }.join
|
162
135
|
end
|
163
136
|
|
164
|
-
|
165
|
-
|
166
|
-
end
|
137
|
+
private
|
138
|
+
|
139
|
+
def js_wrapper(inner) %Q{<script src="#{inner}"></script>} end
|
140
|
+
def css_wrapper(inner) %Q{<link type="text/css" rel="stylesheet" href="#{inner}" />} end
|
167
141
|
|
168
142
|
def with_content_length(response)
|
169
143
|
status, headers, body = response
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "digest/md5"
|
2
|
+
|
3
|
+
require "pinion/asset"
|
4
|
+
require "pinion/error"
|
5
|
+
|
6
|
+
module Pinion
|
7
|
+
class StaticAsset < Asset
|
8
|
+
def initialize(virtual_path, real_path)
|
9
|
+
raise Error, "Bad path for static file: '#{real_path}'." unless File.file? real_path
|
10
|
+
@real_path = real_path
|
11
|
+
@virtual_path = virtual_path
|
12
|
+
temp_contents = contents
|
13
|
+
@length = Rack::Utils.bytesize(temp_contents)
|
14
|
+
@mtime = latest_mtime
|
15
|
+
base, dot, @extension = virtual_path.rpartition(".")
|
16
|
+
@checksum = Digest::MD5.hexdigest(temp_contents)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Don't cache (possibly large) static files in memory
|
20
|
+
def contents() File.read(@real_path) end
|
21
|
+
|
22
|
+
def latest_mtime() File.stat(@real_path).mtime end
|
23
|
+
|
24
|
+
def invalidate
|
25
|
+
Asset.cached_assets.delete(@virtual_path)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
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.
|
4
|
+
version: 0.2.0
|
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-07-
|
12
|
+
date: 2012-07-31 00:00:00.000000000Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rack
|
16
|
-
requirement: &
|
16
|
+
requirement: &15960660 !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: *15960660
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: rake
|
27
|
-
requirement: &
|
27
|
+
requirement: &15959300 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,7 +32,62 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *15959300
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: yard
|
38
|
+
requirement: &15958080 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *15958080
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: scope
|
49
|
+
requirement: &15956220 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *15956220
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: rack-test
|
60
|
+
requirement: &15955100 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *15955100
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: coffee-script
|
71
|
+
requirement: &15947620 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *15947620
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: dedent
|
82
|
+
requirement: &15946140 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: *15946140
|
36
91
|
description: ! 'Pinion is a Rack application that you can use to compile and serve
|
37
92
|
assets (such as Javascript and CSS).
|
38
93
|
|
@@ -44,13 +99,16 @@ extensions: []
|
|
44
99
|
extra_rdoc_files: []
|
45
100
|
files:
|
46
101
|
- README.md
|
47
|
-
- lib/pinion
|
48
|
-
- lib/pinion/
|
102
|
+
- lib/pinion.rb
|
103
|
+
- lib/pinion/bundle.rb
|
49
104
|
- lib/pinion/error.rb
|
105
|
+
- lib/pinion/compiled_asset.rb
|
106
|
+
- lib/pinion/asset.rb
|
107
|
+
- lib/pinion/bundle_type.rb
|
50
108
|
- lib/pinion/server.rb
|
51
|
-
- lib/pinion/
|
109
|
+
- lib/pinion/conversion.rb
|
110
|
+
- lib/pinion/static_asset.rb
|
52
111
|
- lib/pinion/version.rb
|
53
|
-
- lib/pinion.rb
|
54
112
|
homepage: https://github.com/ooyala/pinion
|
55
113
|
licenses: []
|
56
114
|
post_install_message:
|
@@ -76,3 +134,4 @@ signing_key:
|
|
76
134
|
specification_version: 3
|
77
135
|
summary: Pinion compiles and serves your assets
|
78
136
|
test_files: []
|
137
|
+
has_rdoc:
|
@@ -1,47 +0,0 @@
|
|
1
|
-
require "set"
|
2
|
-
require "time"
|
3
|
-
|
4
|
-
module Pinion
|
5
|
-
class DirectoryWatcher
|
6
|
-
# Assume a static filesystem if options[:static] = true. Used in production mode.
|
7
|
-
def initialize(root = ".", options = {})
|
8
|
-
@root = root
|
9
|
-
@watch_directories = Set.new
|
10
|
-
if @static = options[:static]
|
11
|
-
@find_cache = {}
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
def <<(directory) @watch_directories << File.join(@root, directory) end
|
16
|
-
|
17
|
-
def glob(pattern, &block)
|
18
|
-
enumerator = Enumerator.new do |yielder|
|
19
|
-
@watch_directories.each do |directory|
|
20
|
-
Dir.glob(File.join(directory, pattern)) { |filename| yielder.yield filename }
|
21
|
-
end
|
22
|
-
end
|
23
|
-
enumerator.each(&block)
|
24
|
-
end
|
25
|
-
|
26
|
-
def find(path)
|
27
|
-
return @find_cache[path] if (@static && @find_cache.include?(path))
|
28
|
-
result = nil
|
29
|
-
@watch_directories.each do |directory|
|
30
|
-
filename = File.join(directory, path)
|
31
|
-
if File.file? filename
|
32
|
-
result = filename
|
33
|
-
break
|
34
|
-
end
|
35
|
-
end
|
36
|
-
@find_cache[path] = result if @static
|
37
|
-
result
|
38
|
-
end
|
39
|
-
|
40
|
-
def latest_mtime_with_suffix(suffix)
|
41
|
-
pattern = "**/*#{DirectoryWatcher.sanitize_for_glob(suffix)}"
|
42
|
-
glob(pattern).reduce(Time.at(0)) { |latest, path| [latest, File.stat(path).mtime].max }
|
43
|
-
end
|
44
|
-
|
45
|
-
def self.sanitize_for_glob(pattern) pattern.gsub(/[\*\?\[\]\{\}]/) { |match| "\\#{match}" } end
|
46
|
-
end
|
47
|
-
end
|