pinion 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -50,7 +50,7 @@ class YourApp < Sinatra::Base
50
50
  pinion.watch "public/stylus"
51
51
  end
52
52
 
53
- helpers { include Pinion::SinatraHelpers }
53
+ helpers Pinion::SinatraHelpers
54
54
  ...
55
55
  end
56
56
  ```
@@ -117,31 +117,33 @@ Pinion provides some helpers to help you construct links for assets.
117
117
  ```
118
118
 
119
119
  This assumes that you have the `Pinion::Server` instance available inside your app as `pinion`. If you're
120
- using sinatra and `Pinion::SinatraHelpers`, then the helpers are available right in your app's scope:
120
+ using Sinatra and `Pinion::SinatraHelpers`, then the helpers are available right in your app's scope:
121
121
 
122
122
  ``` erb
123
123
  <%= css_url("style.css") %>
124
124
  ```
125
125
 
126
- # In Production
126
+ ## In Production
127
127
 
128
128
  In production, you may wish to concatenate and minify your assets before you serve them. This is done through
129
129
  using asset bundles. Pinion provides a predefined bundle type, `:concatenate_and_uglify_js`, for your
130
130
  convenience.
131
131
 
132
- You can bundle files by putting this in your app:
132
+ You will create bundles when you set up your `Pinion::Server` instance:
133
+
134
+ ``` ruby
135
+ pinion.create_bundle(:main_bundle, :concatenate_and_uglify_js, ["app.js", "util.js", "jquery.js"])
136
+ ```
137
+
138
+ In this case, `:main_bundle` is an identifier for this bundle, and will the name under which this bundle is
139
+ served. In your view, you will use the bundle similarly to how you use the `js_url` or `css_url` helpers:
133
140
 
134
141
  ``` erb
135
- <%= pinion.js_bundle(:concatenate_and_uglify_js, "main-bundle",
136
- "app.js",
137
- "helpers.js",
138
- "util.js",
139
- "jquery.js"
140
- ) %>
142
+ <%= js_bundle(:main_bundle) %>
141
143
  ```
142
144
 
143
- In development, the individual `<script>` tags for each asset will be emitted; in production, a single asset
144
- (`main-bundle.js`) will be produced.
145
+ In development, the individual `<script>` tags for each asset will be emitted; in production, a single
146
+ asset (`main-bundle.js`) will be produced.
145
147
 
146
148
  The `:concatenate_and_uglify_js` bundle type simply concatenates JS files and runs them through
147
149
  [Uglifier](https://github.com/lautis/uglifier). No default CSS bundle type is provided (but the built-in Sass
@@ -168,6 +170,15 @@ Note that in production mode, asset URLs will have the md5sum of the asset inser
168
170
 
169
171
  and these assets are served with long (1-year) expiry, for good cacheability.
170
172
 
173
+ # Example
174
+
175
+ You can see an example app using Pinion and Sinatra in the `example/` directory. This app shows serving some
176
+ static and compiled assets as well as a simple asset bundle. Run `bundle install` in that directory to get the
177
+ necessary gems, then run it:
178
+
179
+ rackup config.ru # Development mode
180
+ RACK_ENV=production rackup config.ru # Production mode
181
+
171
182
  # Notes
172
183
 
173
184
  * Currently, Pinion sidesteps the dependency question by invalidating its cache of each file of a particular
@@ -176,12 +187,39 @@ and these assets are served with long (1-year) expiry, for good cacheability.
176
187
  intance, if `foo/bar` and `foo/baz` are both on the watch list, and both of the files `foo/bar/style.scss`
177
188
  and `foo/baz/style.scss` exist, then `foo/bar/style.scss` will be used if a request occurs for
178
189
  `/style.css`.)
179
-
180
- You can see an example app using Pinion and Sinatra in the `example/` directory. Run `bundle install` in that
181
- directory to get the necessary gems, then run it:
182
-
183
- rackup config.ru # Development mode
184
- RACK_ENV=production rackup config.ru # Production mode
190
+ * If you don't use the url helpers provided by Pinion and instead just serve assets with a plain url (that is,
191
+ without a checksum in the url), Pinion will serve the assets with 10-minute expiry in the Cache-Control
192
+ header. In general, you should try to serve all your assets by using the urls given by the helpers.
193
+
194
+ ## Why not use Sprockets?
195
+
196
+ You should! [Sprockets](https://github.com/sstephenson/sprockets/) is a great project. Pinion is a smaller
197
+ project than Sprockets, and it does fewer things. I made Pinion because I'm not using Rails (the Rails
198
+ integration is where Sprockets really shines) and because some of the design choices that Sprockets makes
199
+ don't really fit my ideal workflow. Here are a few things that Pinion does differently:
200
+
201
+ * Conversions are defined from one filetype to another (e.g. `:scss => :css`), not by the chain of file
202
+ extensions (e.g. `foo.css.scss`). I like the asset manager to follow the file naming scheme, not the other
203
+ way around.
204
+ * It's very easy to define your own conversions instead of using the built-in ones (and this is expected, as
205
+ Pinion only comes with a few of them). This makes it very simple to customize the behavior for your own
206
+ needs. Want to output minified css in production but a human-readable version in dev? This is easy to do
207
+ (and this is the default behavior of the built-in sass/scss converters).
208
+ * To support concatenation, Sprockets uses special directives in comments at the beginning of files (e.g.
209
+ `//= require jquery`) to specify dependencies between files. To my mind, this is not desirable because:
210
+
211
+ * It's easier to debug in development if the files aren't all concatentated together.
212
+ * Other systems (say, a node.js-based JS test runner) don't understand these dependencies.
213
+ * How you bundle assets is mostly a caching/performance concern separate from your app logic, so it
214
+ doesn't necessarily make sense to tie them together. For instance, you may wish to bundle together your
215
+ vendored JS separately from your application JS if you expect the latter to change much more frequently.
216
+
217
+ In contrast, Pinion bundles are created by using the `*_bundle` methods in your app server (view) code,
218
+ which (in my opinion) is much more obvious behavior.
219
+
220
+ This being said, you should certainly consider using Sprockets if it fits into your project. It is a very
221
+ established project that powers the Rails asset pipeline, and it has great support for a wide variety of
222
+ conversion types. If you use both, let me know what you think!
185
223
 
186
224
  # Authors
187
225
 
data/lib/pinion/bundle.rb CHANGED
@@ -8,41 +8,47 @@ module Pinion
8
8
  # A `Bundle` is a set of assets of the same type that will be served as a single grouped asset in
9
9
  # production. A `Bundle` has a `BundleType` that defines how to process the bundle.
10
10
  class Bundle < Asset
11
+ # Each bundle is cached by name.
11
12
  @@bundles = {}
12
13
 
13
- attr_reader :contents, :name
14
+ attr_reader :contents, :name, :paths
14
15
 
15
16
  # Create a new `Bundle`.
16
- def initialize(bundle_type, name, assets)
17
+ def initialize(bundle_type, name, paths)
17
18
  @bundle_type = bundle_type
18
19
  @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 }
20
+ @paths = paths
21
+ raise Error, "No paths provided" if paths.empty?
22
+
23
+ @assets = paths.map do |path|
24
+ asset = Asset[path]
25
+ raise "Error: no such asset available: #{path}" unless asset
26
+ asset
27
+ end
28
+ @extension = @assets.first.extension
29
+ unless @assets.all? { |asset| asset.extension == @extension }
23
30
  raise Error, "All assets in a bundle must have the same extension"
24
31
  end
25
- @contents = bundle_type.process(assets)
32
+ @contents = bundle_type.process(@assets)
26
33
  @checksum = Digest::MD5.hexdigest(@contents)
27
- @mtime = assets.map(&:mtime).max
34
+ @mtime = @assets.map(&:mtime).max
28
35
  @length = Rack::Utils.bytesize(@contents)
29
36
  end
30
37
 
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)
38
+ # Create a new bundle from a bundle_type name (e.g. `:concatenate_and_uglify_js`) and an array of paths.
39
+ # The name is taken as the identifier in the resulting path.
40
+ def self.create(name, bundle_type_name, paths)
34
41
  bundle_type = BundleType[bundle_type_name]
35
42
  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
43
+ if @@bundles[name.to_s]
44
+ raise Error, "There is already a bundle called #{name}. Each bundle must have a different name."
45
+ end
46
+ bundle = Bundle.new(bundle_type, name, paths)
47
+ @@bundles[name.to_s] = bundle
38
48
  bundle
39
49
  end
40
50
 
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
51
+ # Find a `Bundle` by its name.
52
+ def self.[](name) name && @@bundles[name.to_s] end
47
53
  end
48
54
  end
data/lib/pinion/server.rb CHANGED
@@ -64,14 +64,30 @@ module Pinion
64
64
  path.sub!("-#{checksum_tag}", "")
65
65
  end
66
66
 
67
- asset = Bundle[bundle_name, checksum_tag] || Asset[path]
67
+ # First see if there is a bundle with this name
68
+ asset = Bundle[bundle_name]
69
+ if asset
70
+ # If the checksum doesn't match the bundle, we want to return a 404.
71
+ asset = nil unless asset.checksum == checksum_tag
72
+ else
73
+ # If there is no bundle with this name, find another type of Asset with the name instead.
74
+ asset = Asset[path]
75
+ end
68
76
 
69
77
  if asset
70
78
  # If the ETag matches, give a 304
71
79
  return [304, {}, []] if env["HTTP_IF_NONE_MATCH"] == %Q["#{asset.checksum}"]
72
80
 
73
- # Cache for a year in production; don't cache in dev
74
- cache_policy = checksum_tag ? "max-age=31536000" : "must-revalidate"
81
+ if Pinion.environment == "production"
82
+ # In production, if there is a checksum, cache for a year (because if the asset changes with a
83
+ # redeploy, the checksum will change). If there is no checksum, cache for 10 minutes (this way at
84
+ # worst we only serve 10 minute stale assets, and caches in front of Pinion will be able to help
85
+ # out).
86
+ cache_policy = checksum_tag ? "max-age=31536000" : "max-age=600"
87
+ else
88
+ # Don't cache in dev.
89
+ cache_policy = "must-revalidate"
90
+ end
75
91
  headers = {
76
92
  "Content-Type" => asset.content_type,
77
93
  "Content-Length" => asset.length.to_s,
@@ -113,29 +129,36 @@ module Pinion
113
129
  def css_inline(path) %Q{<style type="text/css">#{asset_inline(path)}</style>} end
114
130
  def js_inline(path) %Q{<script>#{asset_inline(path)}</script>} end
115
131
 
116
- # Bundle several assets together. In production, the single bundled result is produced; otherwise, each
117
- # individual asset_url is returned.
118
- def bundle_url(bundle_name, name, *paths)
119
- return paths.map { |p| asset_url(p) } unless Pinion.environment == "production"
120
- paths.each { |path| path.sub!(%r[^(#{@mount_point})?/?], "") }
121
- assets = paths.map do |path|
122
- asset = Asset[path]
123
- raise "Error: no such asset available: #{path}" unless asset
124
- asset
132
+ # Create a bundle of assets. Each asset must convert to the same final type. `name` is an identifier for
133
+ # this bundle; `bundle_name` is the # identifier for your bundle type (e.g. `:concatenate_and_uglify_js`);
134
+ # and `paths` are all the asset paths. In development, no bundles will be created (but the list of
135
+ # discrete assets will be saved for use in a subsequent `bundle_url` call).
136
+ def create_bundle(name, bundle_name, paths)
137
+ if Bundle[name]
138
+ raise "Error: there is already a bundle called #{name} with different component files. Each " <<
139
+ "bundle must have a unique name."
125
140
  end
126
- bundle = Bundle.create(bundle_name, name, assets)
127
- ["#{@mount_point}/#{bundle.name}-#{bundle.checksum}.#{bundle.extension}"]
128
- end
129
- def js_bundle(bundle_name, name, *paths)
130
- bundle_url(bundle_name, name, *paths).map { |path| js_wrapper(path) }.join
141
+
142
+ normalized_paths = paths.map { |path| path.sub(%r[^(#{@mount_point})?/?], "") }
143
+ Bundle.create(name, bundle_name, normalized_paths)
131
144
  end
132
- def css_bundle(bundle_name, name, *paths)
133
- bundle_url(bundle_name, name, *paths).map { |path| css_wrapper(path) }.join
145
+
146
+ # Return the bundle url. In production, the single bundled result is produced; otherwise, each individual
147
+ # asset_url is returned.
148
+ def bundle_url(name)
149
+ bundle = Bundle[name]
150
+ raise "No such bundle: #{name}" unless bundle
151
+ return bundle.paths.map { |p| asset_url(p) } unless Pinion.environment == "production"
152
+ ["#{@mount_point}/#{bundle.name}-#{bundle.checksum}.#{bundle.extension}"]
134
153
  end
154
+ def js_bundle(name) bundle_url(name).map { |path| js_wrapper(path) }.join end
155
+ def css_bundle(name) bundle_url(name).map { |path| css_wrapper(path) }.join end
135
156
 
136
157
  private
137
158
 
138
- def js_wrapper(inner) %Q{<script src="#{inner}"></script>} end
159
+ # Need type="text/javascript" for IE compatibility. The content-type will be set correctly as
160
+ # "application/javascript" in the response.
161
+ def js_wrapper(inner) %Q{<script type="text/javascript" src="#{inner}"></script>} end
139
162
  def css_wrapper(inner) %Q{<link type="text/css" rel="stylesheet" href="#{inner}" />} end
140
163
 
141
164
  def with_content_length(response)
@@ -9,9 +9,7 @@ module Pinion
9
9
  #
10
10
  # then mix in in this module in your sinatra app:
11
11
  #
12
- # helpers do
13
- # include Pinion::SinatraHelpers
14
- # end
12
+ # helpers Pinion::SinatraHelpers
15
13
  #
16
14
  # Now you can access the Pinion::Server helper methods in your view:
17
15
  #
@@ -1,3 +1,3 @@
1
1
  module Pinion
2
- VERSION = "0.2.2"
2
+ VERSION = "0.3.0"
3
3
  end
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.2.2
4
+ version: 0.3.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-08-03 00:00:00.000000000Z
12
+ date: 2012-08-17 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack
16
- requirement: &20353460 !ruby/object:Gem::Requirement
16
+ requirement: &26493960 !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: *20353460
24
+ version_requirements: *26493960
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rake
27
- requirement: &20353040 !ruby/object:Gem::Requirement
27
+ requirement: &26493500 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *20353040
35
+ version_requirements: *26493500
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: yard
38
- requirement: &20352580 !ruby/object:Gem::Requirement
38
+ requirement: &26493020 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *20352580
46
+ version_requirements: *26493020
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: scope
49
- requirement: &20352160 !ruby/object:Gem::Requirement
49
+ requirement: &26492500 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: '0'
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *20352160
57
+ version_requirements: *26492500
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: rack-test
60
- requirement: &20351740 !ruby/object:Gem::Requirement
60
+ requirement: &26492020 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ! '>='
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: '0'
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *20351740
68
+ version_requirements: *26492020
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: coffee-script
71
- requirement: &20351320 !ruby/object:Gem::Requirement
71
+ requirement: &26491360 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ! '>='
@@ -76,10 +76,10 @@ dependencies:
76
76
  version: '0'
77
77
  type: :development
78
78
  prerelease: false
79
- version_requirements: *20351320
79
+ version_requirements: *26491360
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: dedent
82
- requirement: &20350900 !ruby/object:Gem::Requirement
82
+ requirement: &26490460 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - ! '>='
@@ -87,7 +87,7 @@ dependencies:
87
87
  version: '0'
88
88
  type: :development
89
89
  prerelease: false
90
- version_requirements: *20350900
90
+ version_requirements: *26490460
91
91
  description: ! 'Pinion is a Rack application that you can use to compile and serve
92
92
  assets (such as Javascript and CSS).
93
93