pinion 0.1.6 → 0.2.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/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
|