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 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
- <!-- Shorthand equivalent -->
63
+ <%# Shorthand equivalent %>
68
64
  <%= pinion.css_url("style.css") %>
69
65
  </head>
70
66
  ```
71
67
 
72
- Notes
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) and [rerun](https://github.com/alexch/rerun).
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 "digest/md5"
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
- 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)
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 @compiled_contents end
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
@@ -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
- @environment = {}
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, @environment)
44
+ @conversion_fn.call(file_contents, @context)
52
45
  end
53
- def add_watch_directory(path) @watch_fn.call(path, @environment) end
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, environment|
82
- load_paths = environment[:load_paths].to_a || []
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, environment|
86
- environment[:load_paths] ||= Set.new
87
- environment[:load_paths] << path
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, environment|
94
- load_paths = environment[:load_paths].to_a || []
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, environment|
98
- environment[:load_paths] ||= Set.new
99
- environment[:load_paths] << path
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
- @watcher << path
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 = path[/-([\da-f]{32})\..+$/, 1]
62
- path.sub!("-#{checksum_tag}", "") if checksum_tag
63
-
64
- real_file = @watcher.find path
65
- if real_file
66
- # Total hack; this is probably a big misuse of Rack::File but I don't want to have to reproduce a lot
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 = find_asset(path)
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
- return [200, headers, []] if env["REQUEST_METHOD"] == "HEAD"
89
- [200, headers, asset]
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
- # 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"
101
+ return mounted_path unless Pinion.environment == "production"
110
102
 
111
103
  # Add on a checksum tag in production
112
- asset = find_asset(path)
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) %Q{<link type="text/css" rel="stylesheet" href="#{asset_url(path)}" />} end
119
- def js_url(path) %Q{<script src="#{asset_url(path)}"></script>} end
120
-
121
- private
122
-
123
- def find_asset(to_path)
124
- asset = @cached_assets[to_path]
125
- if asset
126
- return asset if @environment == "production"
127
- mtime = asset.mtime
128
- latest = @watcher.latest_mtime_with_suffix(asset.from_type.to_s)
129
- if latest > mtime
130
- invalidate_all_assets_of_type(asset.from_type)
131
- return find_asset(to_path)
132
- end
133
- else
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
- asset
127
+ bundle = Bundle.create(bundle_name, name, assets)
128
+ ["#{@mount_point}/#{bundle.name}-#{bundle.checksum}.#{bundle.extension}"]
142
129
  end
143
-
144
- def find_uncached_asset(to_path)
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
- def find_source_file_and_conversion(to_path)
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
- def invalidate_all_assets_of_type(type)
165
- @cached_assets.delete_if { |to_path, asset| asset.from_type == type }
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
@@ -1,3 +1,3 @@
1
1
  module Pinion
2
- VERSION = "0.1.6"
2
+ VERSION = "0.2.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.1.6
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-17 00:00:00.000000000Z
12
+ date: 2012-07-31 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack
16
- requirement: &24716220 !ruby/object:Gem::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: *24716220
24
+ version_requirements: *15960660
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rake
27
- requirement: &24715800 !ruby/object:Gem::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: *24715800
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/asset.rb
48
- - lib/pinion/conversion.rb
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/directory_watcher.rb
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