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 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