darkroom 0.0.1 → 0.0.2

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