darkroom 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15822eea03a5814dc169523ec7285c97b12efa7e3818a5de03099b670823564d
4
- data.tar.gz: 6ab19e03a8282508ea571d1d30b458750aaad273af0151d2582eeff889d69a4e
3
+ metadata.gz: 518b5ff3d82631d41125029a7292c2bfdd8942990a7e92e2be08f99217fe8d02
4
+ data.tar.gz: 388cf8db580df8de6d85a9d196786ef7400a97a096dfbbb316de4b0dbca8e295
5
5
  SHA512:
6
- metadata.gz: a421ab802832dd3268d8fdb17cf1c3843f5bf554ec43eace104b3e324a53bf4790f5268a65b4c1acb5b545782ab69fda5ea5aeebca62b77f3cc1ebe4889aa5d7
7
- data.tar.gz: b040b997cbc321ce11af041f387bb1a89543071b9b657a38575602e8663e87573ff6442af8c016061ce29fdb67284f5301cbcbaa22b14cd8c8d1ce6a61b255db
6
+ metadata.gz: 439b783c5d7afcb20c6a5bde715f686e42236bd70251af909938784f3c4e855f3e22b8fa779b1de19478e64e17462dc4fe37d28f043c0a62f95d13cc8e40829a
7
+ data.tar.gz: 3bb350472717b4a656e71ef3c7c1b8c200b88c7414c3f7feecfee46b19327e7d7bb5bb061878474d584c65afa84777cdf784fe76fd0cada86cecfbb20485fd13
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.1
1
+ 0.0.2
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require('base64')
3
4
  require('digest')
4
5
 
5
6
  class Darkroom
@@ -12,7 +13,7 @@ class Darkroom
12
13
 
13
14
  @@specs = {}
14
15
 
15
- attr_reader(:content, :error, :errors, :path, :path_versioned)
16
+ attr_reader(:content, :error, :errors, :path, :path_unversioned, :path_versioned)
16
17
 
17
18
  ##
18
19
  # Holds information about how to handle a particular asset type.
@@ -72,18 +73,21 @@ class Darkroom
72
73
  #
73
74
  # * +file+ - The path to the file on disk.
74
75
  # * +path+ - The path this asset will be referenced by (e.g. /js/app.js).
75
- # * +manifest+ - Manifest hash from the Darkroom instance that the asset is a member of.
76
+ # * +darkroom+ - Darkroom instance that the asset is a member of.
77
+ # * +prefix+ - Prefix to apply to unversioned and versioned paths.
76
78
  # * +minify+ - Boolean specifying whether or not the asset should be minified when processed.
77
79
  # * +internal+ - Boolean indicating whether or not the asset is only accessible internally (i.e. as a
78
80
  # dependency).
79
81
  #
80
- def initialize(path, file, manifest, minify: false, internal: false)
82
+ def initialize(path, file, darkroom, prefix: nil, minify: false, internal: false)
81
83
  @path = path
82
84
  @file = file
83
- @manifest = manifest
85
+ @darkroom = darkroom
86
+ @prefix = prefix
84
87
  @minify = minify
85
88
  @internal = internal
86
89
 
90
+ @path_unversioned = "#{@prefix}#{@path}"
87
91
  @extension = File.extname(@path).downcase
88
92
  @spec = self.class.spec(@extension) or raise(SpecNotDefinedError.new(@extension, @file))
89
93
 
@@ -92,35 +96,26 @@ class Darkroom
92
96
  end
93
97
 
94
98
  ##
95
- # Processes the asset if necessary (file's mtime is compared to the last time it was processed). File is
96
- # read from disk, any dependencies are merged into its content (if spec for the asset type allows for
97
- # it), the content is compiled (if the asset type requires compilation), and minified (if specified for
98
- # this Asset). Returns true if asset was modified since it was last processed and false otherwise.
99
+ # Processes the asset if modified (see #modified? for how modification is determined). File is read from
100
+ # disk, any dependencies are merged into its content (if spec for the asset type allows for it), the
101
+ # content is compiled (if the asset type requires compilation), and minified (if specified for this
102
+ # Asset). Returns true if asset was modified since it was last processed and false otherwise.
99
103
  #
100
- # * +key+ - Unique value associated with the current round of processing.
101
- #
102
- def process(key)
103
- key == @process_key ? (return @modified) : (@process_key = key)
104
-
105
- begin
106
- @modified = @mtime != (@mtime = File.mtime(@file))
107
- @modified ||= @dependencies.any? { |d| d.process(key) }
108
-
109
- return false unless @modified
110
- rescue Errno::ENOENT
111
- clear
112
- return @modified = true
113
- end
104
+ def process
105
+ @process_key == @darkroom.process_key ? (return @processed) : (@process_key = @darkroom.process_key)
106
+ modified? ? (@processed = true) : (return @processed = false)
114
107
 
115
108
  clear
116
- read(key)
109
+ read
117
110
  compile
118
111
  minify
119
112
 
120
113
  @fingerprint = Digest::MD5.hexdigest(@content)
121
- @path_versioned = @path.sub(EXTENSION_REGEX, "-#{@fingerprint}")
114
+ @path_versioned = "#{@prefix}#{@path.sub(EXTENSION_REGEX, "-#{@fingerprint}")}"
122
115
 
123
- @modified
116
+ @processed
117
+ rescue Errno::ENOENT
118
+ # File was deleted. Do nothing.
124
119
  ensure
125
120
  @error = @errors.empty? ? nil : ProcessingError.new(@errors)
126
121
  end
@@ -145,6 +140,23 @@ class Darkroom
145
140
  }.compact!
146
141
  end
147
142
 
143
+ ##
144
+ # Returns subresource integrity string.
145
+ #
146
+ # * +algorithm+ - The hash algorithm to use to generate the integrity string (one of sha256, sha384, or
147
+ # sha512).
148
+ #
149
+ def integrity(algorithm = :sha384)
150
+ @integrity[algorithm] ||= "#{algorithm}-#{Base64.strict_encode64(
151
+ case algorithm
152
+ when :sha256 then Digest::SHA256.digest(@content)
153
+ when :sha384 then Digest::SHA384.digest(@content)
154
+ when :sha512 then Digest::SHA512.digest(@content)
155
+ else raise("Unrecognized integrity algorithm: #{algorithm}")
156
+ end
157
+ )}".freeze
158
+ end
159
+
148
160
  ##
149
161
  # Returns boolean indicating whether or not the asset is marked as internal.
150
162
  #
@@ -173,24 +185,57 @@ class Darkroom
173
185
  "@minify=#{@minify.inspect}, "\
174
186
  "@mtime=#{@mtime.inspect}, "\
175
187
  "@path=#{@path.inspect}, "\
176
- "@path_versioned=#{@path_versioned.inspect}"\
188
+ "@path_unversioned=#{@path_unversioned.inspect}, "\
189
+ "@path_versioned=#{@path_versioned.inspect}, "\
190
+ "@prefix=#{@prefix.inspect}"\
177
191
  '>'
178
192
  end
179
193
 
180
194
  protected
181
195
 
182
196
  ##
183
- # Returns the processed content of the asset without dependencies concatenated.
197
+ # Returns true if the asset or any of its dependencies were modified since last processed, or if an
198
+ # error was recorded during the last processing run.
184
199
  #
185
- def own_content
186
- @own_content
200
+ def modified?
201
+ @modified_key == @darkroom.process_key ? (return @modified) : (@modified_key = @darkroom.process_key)
202
+
203
+ begin
204
+ @modified = !!@error
205
+ @modified ||= @mtime != (@mtime = File.mtime(@file))
206
+ @modified ||= dependencies.any? { |d| d.modified? }
207
+ rescue Errno::ENOENT
208
+ @modified = true
209
+ end
187
210
  end
188
211
 
189
212
  ##
190
- # Returns an array of all the asset's dependencies.
213
+ # Returns all dependencies (including dependencies of dependencies).
214
+ #
215
+ # * +ancestors+ - Ancestor chain followed to get to this asset as a dependency.
191
216
  #
192
- def dependencies
193
- @dependencies
217
+ def dependencies(ancestors = Set.new)
218
+ @dependencies ||= @own_dependencies.inject([]) do |dependencies, own_dependency|
219
+ next dependencies if ancestors.include?(self)
220
+
221
+ ancestors << self
222
+ own_dependency.process
223
+
224
+ dependencies |= own_dependency.dependencies(ancestors)
225
+ dependencies |= [own_dependency]
226
+
227
+ dependencies.delete(self)
228
+ ancestors.delete(self)
229
+
230
+ dependencies
231
+ end
232
+ end
233
+
234
+ ##
235
+ # Returns the processed content of the asset without dependencies concatenated.
236
+ #
237
+ def own_content
238
+ @own_content
194
239
  end
195
240
 
196
241
  private
@@ -199,48 +244,40 @@ class Darkroom
199
244
  # Clears content, dependencies, and errors so asset is ready for (re)processing.
200
245
  #
201
246
  def clear
247
+ @dependencies = nil
248
+ @error = nil
249
+ @fingerprint = nil
250
+ @path_versioned = nil
251
+
202
252
  (@errors ||= []).clear
203
- (@dependencies ||= []).clear
253
+ (@own_dependencies ||= []).clear
204
254
  (@content ||= +'').clear
205
255
  (@own_content ||= +'').clear
256
+ (@integrity ||= {}).clear
206
257
  end
207
258
 
208
259
  ##
209
260
  # Reads the asset file, building dependency array if dependencies are supported for the asset's type.
210
261
  #
211
- # * +key+ - Unique value associated with the current round of processing.
212
- #
213
- def read(key)
214
- if @spec.dependency_regex
215
- dependencies = []
216
-
217
- File.new(@file).each.with_index do |line, line_num|
218
- if (path = line[@spec.dependency_regex, :path])
219
- if (dependency = @manifest[path])
220
- dependencies << dependency
221
- else
222
- @errors << AssetNotFoundError.new(path, @path, line_num + 1)
223
- end
262
+ def read
263
+ unless @spec.dependency_regex
264
+ @own_content = File.read(@file)
265
+ return
266
+ end
267
+
268
+ File.new(@file).each.with_index do |line, line_num|
269
+ if (path = line[@spec.dependency_regex, :path])
270
+ if (dependency = @darkroom.manifest(path))
271
+ @own_dependencies << dependency
224
272
  else
225
- @own_content << line
273
+ @errors << AssetNotFoundError.new(path, @path, line_num + 1)
226
274
  end
275
+ else
276
+ @own_content << line
227
277
  end
228
-
229
- dependencies.each do |dependency|
230
- dependency.process(key)
231
-
232
- @dependencies += dependency.dependencies
233
- @dependencies << dependency
234
- end
235
-
236
- @dependencies.uniq!
237
- @dependencies.delete_if { |d| d.path == @path }
238
-
239
- @content << @dependencies.map { |d| d.own_content }.join(DEPENDENCY_JOINER)
240
- @own_content
241
- else
242
- @own_content = File.read(@file)
243
278
  end
279
+
280
+ @content << dependencies.map { |d| d.own_content }.join(DEPENDENCY_JOINER)
244
281
  end
245
282
 
246
283
  ##
@@ -287,14 +324,17 @@ class Darkroom
287
324
  begin
288
325
  require(@spec.compile_lib) if @spec.compile_lib
289
326
  rescue LoadError
290
- raise(MissingLibraryError.new(@spec.compile_lib, 'compile', @extension))
327
+ compile_load_error = true
291
328
  end
292
329
 
293
330
  begin
294
331
  require(@spec.minify_lib) if @spec.minify_lib && @minify
295
332
  rescue LoadError
296
- raise(MissingLibraryError.new(@spec.minify_lib, 'minify', @extension))
333
+ minify_load_error = true
297
334
  end
335
+
336
+ raise(MissingLibraryError.new(@spec.compile_lib, 'compile', @extension)) if compile_load_error
337
+ raise(MissingLibraryError.new(@spec.minify_lib, 'minify', @extension)) if minify_load_error
298
338
  end
299
339
  end
300
340
  end
@@ -13,7 +13,7 @@ class Darkroom
13
13
 
14
14
  TRAILING_SLASHES = /\/+$/.freeze
15
15
 
16
- attr_reader(:error, :errors)
16
+ attr_reader(:error, :errors, :process_key)
17
17
 
18
18
  ##
19
19
  # Creates a new instance.
@@ -51,8 +51,12 @@ class Darkroom
51
51
 
52
52
  @min_process_interval = min_process_interval
53
53
  @last_processed_at = 0
54
+ @process_key = 0
54
55
  @mutex = Mutex.new
56
+
55
57
  @manifest = {}
58
+ @manifest_unversioned = {}
59
+ @manifest_versioned = {}
56
60
 
57
61
  Thread.current[:darkroom_host_index] = -1 unless @hosts.empty?
58
62
  end
@@ -70,6 +74,7 @@ class Darkroom
70
74
  end
71
75
 
72
76
  @mutex.synchronize do
77
+ @process_key += 1
73
78
  @errors = []
74
79
  found = {}
75
80
 
@@ -82,21 +87,28 @@ class Darkroom
82
87
  else
83
88
  found[path] = load_path
84
89
 
85
- @manifest[path] ||= Asset.new(path, file, @manifest,
86
- internal: internal = @internal_pattern && path =~ @internal_pattern,
87
- minify: @minify && !internal && path !~ @minified_pattern,
90
+ @manifest[path] ||= Asset.new(path, file, self,
91
+ prefix: (@prefix unless @pristine.include?(path)),
92
+ internal: @internal_pattern && path =~ @internal_pattern,
93
+ minify: @minify && path !~ @minified_pattern,
88
94
  )
89
95
  end
90
96
  end
91
97
  end
92
98
 
93
99
  @manifest.select! { |path, _| found.key?(path) }
100
+ @manifest_unversioned.clear
101
+ @manifest_versioned.clear
102
+
103
+ @manifest.each do |path, asset|
104
+ asset.process
94
105
 
95
- found.each do |path, _|
96
- @manifest[path].process(@last_processed_at)
97
- @manifest[@manifest[path].path_versioned] = @manifest[path]
106
+ unless asset.internal?
107
+ @manifest_unversioned[asset.path_unversioned] = asset
108
+ @manifest_versioned[asset.path_versioned] = asset
109
+ end
98
110
 
99
- @errors += @manifest[path].errors
111
+ @errors += asset.errors
100
112
  end
101
113
  ensure
102
114
  @last_processed_at = Time.now.to_f
@@ -132,14 +144,7 @@ class Darkroom
132
144
  # * +path+ - The external path of the asset.
133
145
  #
134
146
  def asset(path)
135
- asset = @manifest[@prefix ? path.sub(@prefix, '') : path]
136
-
137
- return nil if asset.nil?
138
- return nil if asset.internal?
139
- return nil if @prefix && !path.start_with?(@prefix) && !@pristine.include?(asset.path)
140
- return nil if @prefix && path.start_with?(@prefix) && @pristine.include?(asset.path)
141
-
142
- asset
147
+ @manifest_versioned[path] || @manifest_unversioned[path]
143
148
  end
144
149
 
145
150
  ##
@@ -151,28 +156,40 @@ class Darkroom
151
156
  # darkroom.asset_path('/js/app.js') # => /assets/js/app.<hash>.js
152
157
  # darkroom.asset_path('/js/app.js', versioned: false) # => /assets/js/app.js
153
158
  #
159
+ # Raises an AssetNotFoundError if the asset doesn't exist.
160
+ #
154
161
  # * +path+ - The internal path of the asset.
155
162
  # * +versioned+ - Boolean indicating whether the versioned or unversioned path should be returned.
156
163
  #
157
164
  def asset_path(path, versioned: !@pristine.include?(path))
158
- asset = @manifest[path] or return nil
159
-
165
+ asset = @manifest[path] or raise(AssetNotFoundError.new(path))
160
166
  host = @hosts.empty? ? '' : @hosts[
161
167
  Thread.current[:darkroom_host_index] = (Thread.current[:darkroom_host_index] + 1) % @hosts.size
162
168
  ]
163
- prefix = @prefix unless @pristine.include?(path)
164
169
 
165
- "#{host}#{prefix}#{versioned ? asset.path_versioned : path}"
170
+ "#{host}#{versioned ? asset.path_versioned : asset.path_unversioned}"
166
171
  end
167
172
 
168
173
  ##
169
- # Calls #asset_path and raises a AssetNotFoundError if the asset doesn't exist (instead of just returning
170
- # nil).
174
+ # Returns an asset's subresource integrity string. Raises an AssetNotFoundError if the asset doesn't
175
+ # exist.
171
176
  #
172
- # * +versioned+ - Boolean indicating whether the versioned or unversioned path should be returned.
177
+ # * +path+ - The internal path of the asset.
178
+ # * +algorithm+ - The hash algorithm to use to generate the integrity string (see Asset#integrity).
173
179
  #
174
- def asset_path!(path, versioned: true)
175
- asset_path(path, versioned: versioned) or raise(AssetNotFoundError.new(path))
180
+ def asset_integrity(path, algorithm = nil)
181
+ asset = @manifest[path] or raise(AssetNotFoundError.new(path))
182
+
183
+ algorithm ? asset.integrity(algorithm) : asset.integrity
184
+ end
185
+
186
+ ##
187
+ # Returns the asset from the manifest hash associated with the given path.
188
+ #
189
+ # * +path+ - The internal path of the asset.
190
+ #
191
+ def manifest(path)
192
+ @manifest[path]
176
193
  end
177
194
 
178
195
  ##
@@ -188,23 +205,20 @@ class Darkroom
188
205
  #
189
206
  def dump(dir, clear: false, include_pristine: true)
190
207
  dir = File.expand_path(dir)
191
- written = Set.new
192
208
 
193
209
  FileUtils.mkdir_p(dir)
194
210
  Dir.each_child(dir) { |child| FileUtils.rm_rf(File.join(dir, child)) } if clear
195
211
 
196
- @manifest.each do |_, asset|
212
+ @manifest_versioned.each do |path, asset|
197
213
  next if asset.internal?
198
- next if written.include?(asset.path)
199
214
  next if @pristine.include?(asset.path) && !include_pristine
200
215
 
201
- external_path = asset_path(asset.path)
202
- file_path = File.join(dir, external_path)
216
+ file_path = File.join(dir,
217
+ @pristine.include?(asset.path) ? asset.path_unversioned : path
218
+ )
203
219
 
204
220
  FileUtils.mkdir_p(File.dirname(file_path))
205
221
  File.write(file_path, asset.content)
206
-
207
- written << asset.path
208
222
  end
209
223
  end
210
224
 
@@ -222,7 +236,8 @@ class Darkroom
222
236
  "@minified_pattern=#{@minified_pattern.inspect}, "\
223
237
  "@minify=#{@minify.inspect}, "\
224
238
  "@prefix=#{@prefix.inspect}, "\
225
- "@pristine=#{@pristine.inspect}"\
239
+ "@pristine=#{@pristine.inspect}, "\
240
+ "@process_key=#{@process_key.inspect}"\
226
241
  '>'
227
242
  end
228
243
  end
@@ -24,7 +24,8 @@ class Darkroom
24
24
  # Returns a string representation of the error.
25
25
  #
26
26
  def to_s
27
- "Cannot #{@need} #{@extension} file(s): #{@library} library not available"
27
+ "Cannot #{@need} #{@extension} file(s): #{@library} library not available [hint: try adding "\
28
+ "gem('#{@library}') to your Gemfile]"
28
29
  end
29
30
  end
30
31
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Darkroom
4
- VERSION = '0.0.1'
4
+ VERSION = '0.0.2'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: darkroom
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nate Pickens
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-02-25 00:00:00.000000000 Z
11
+ date: 2021-03-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler