darkroom 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 15822eea03a5814dc169523ec7285c97b12efa7e3818a5de03099b670823564d
4
+ data.tar.gz: 6ab19e03a8282508ea571d1d30b458750aaad273af0151d2582eeff889d69a4e
5
+ SHA512:
6
+ metadata.gz: a421ab802832dd3268d8fdb17cf1c3843f5bf554ec43eace104b3e324a53bf4790f5268a65b4c1acb5b545782ab69fda5ea5aeebca62b77f3cc1ebe4889aa5d7
7
+ data.tar.gz: b040b997cbc321ce11af041f387bb1a89543071b9b657a38575602e8663e87573ff6442af8c016061ce29fdb67284f5301cbcbaa22b14cd8c8d1ce6a61b255db
data/LICENSE ADDED
@@ -0,0 +1,16 @@
1
+ Copyright 2021 Nate Pickens
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
4
+ documentation files (the "Software"), to deal in the Software without restriction, including without
5
+ limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
6
+ the Software, and to permit persons to whom the Software is furnished to do so, subject to the following
7
+ conditions:
8
+
9
+ The above copyright notice and this permission notice shall be included in all copies or substantial
10
+ portions of the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
13
+ LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
14
+ EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
15
+ AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
16
+ OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # Darkroom
2
+
3
+ Darkroom is a fast, lightweight, and straightforward web asset management library. Processed assets are all
4
+ stored in and served directly from memory rather than being written to disk (though a dump to disk can be
5
+ performed for upload to a CDN or proxy server in production environments); this keeps asset management
6
+ simple and performant in development. Darkroom also supports asset bundling for CSS and JavaScript using
7
+ each language's native import statement syntax.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your Gemfile:
12
+
13
+ ```ruby
14
+ gem('darkroom')
15
+ ```
16
+
17
+ Or install manually on the command line:
18
+
19
+ ```bash
20
+ gem install darkroom
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ To create and start using a Darkroom instance, specify one or more load paths (all other arguments are
26
+ optional):
27
+
28
+ ```ruby
29
+ darkroom = Darkroom.new('app/assets', 'vendor/assets', '...',
30
+ hosts: ['https://cdn1.com', '...'] # Hosts to prepend to asset paths (useful in production)
31
+ prefix: '/static', # Prefix to add to all asset paths
32
+ pristine: ['/google-verify.html'], # Paths with no prefix or versioning (e.g. /favicon.ico)
33
+ minify: true, # Minify assets that can be minified
34
+ minified_pattern: /(\.|-)min\.\w+$/, # Files that should not be minified
35
+ internal_pattern: /^\/components\//, # Files that cannot be accessed directly
36
+ min_process_interval: 1, # Minimum time that must elapse between process calls
37
+ )
38
+
39
+ # Refreshe any assets that have been modified (in development, this should be called at the
40
+ # beginning of each web request).
41
+ darkroom.process
42
+
43
+ # Dump assets to disk. Useful when deploying to a production environment where assets will be
44
+ # uploaded to and served from a CDN or proxy server.
45
+ darkroom.dump('output/dir',
46
+ clear: true, # Delete contents of output/dir before dumping
47
+ include_pristine: true, # Include pristine assets (if preparing for CDN upload, files like
48
+ ) # /favicon.ico or /robots.txt should be left out)
49
+ ```
50
+
51
+ Note that assets paths across all load path directories must be globally unique (e.g. the existance of both
52
+ `app/assets/app.js` and `vendor/assets/app.js` will result in an error).
53
+
54
+ To work with assets:
55
+
56
+ ```ruby
57
+ # Get the external path that will be used by HTTP requests.
58
+ path = darkroom.asset_path('/js/app.js') # => '/static/js/app-<fingerprint>.js'
59
+
60
+ # Retrieve the Asset object associated with a path.
61
+ asset = darkroom.asset(path)
62
+
63
+ # Getting paths directly from an Asset object will not include any host or prefix.
64
+ assest.path # => '/js/app.js'
65
+ assest.path_versioned # => '/js/app-<fingerprint>.js'
66
+
67
+ asset.content_type # => 'application/javascript'
68
+ asset.content # Content of processed /js/app.js file
69
+
70
+ asset.headers # => {'Content-Type' => 'application/javascript',
71
+ # 'Cache-Control' => 'public, max-age=31536000'}
72
+ asset.headers(versioned: false) # => {'Content-Type' => 'application/javascript',
73
+ # 'ETag' => '<fingerprint>'}
74
+ ```
75
+
76
+ ## Asset Bundling
77
+
78
+ CSS and JavaScript assets specify their dependencies by way of each language's native import statement. Each
79
+ import statement is replaced with content of the referenced asset. Example:
80
+
81
+ ```javascript
82
+ // Unprocessed /api.js
83
+ function api() {
84
+ console.log('API called!')
85
+ }
86
+
87
+ // Unprocessed /app.js
88
+ import '/api.js'
89
+
90
+ api()
91
+
92
+ // Processed /app.js
93
+ function api() {
94
+ console.log('API called!')
95
+ }
96
+
97
+
98
+ api()
99
+ ```
100
+
101
+ The same applies for CSS files. Example:
102
+
103
+ ```css
104
+ /* Unprocessed /header.css */
105
+ header {
106
+ background: #f1f1f1;
107
+ }
108
+
109
+ /* Unprocessed /app.css */
110
+ @import '/header.css';
111
+
112
+ body {
113
+ background: #fff;
114
+ }
115
+
116
+ /* Processed /app.css */
117
+ header {
118
+ background: #f1f1f1;
119
+ }
120
+
121
+
122
+ body {
123
+ background: #fff;
124
+ }
125
+ ```
126
+
127
+ Imported assets can also contain import statements, and those assets are all included in the base asset.
128
+ Imports can even be cyclical. If `asset-a.css` imports `asset-b.css` and vice-versa, each asset will simply
129
+ contain the content of both of those assets (though order will be different as an asset's own content always
130
+ comes after any imported assets' contents).
131
+
132
+ ## Contributing
133
+
134
+ Bug reports and pull requests are welcome on GitHub at https://github.com/npickens/darkroom.
135
+
136
+ ## License
137
+
138
+ The gem is available as open source under the terms of the
139
+ [MIT License](https://opensource.org/licenses/MIT).
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
data/lib/darkroom.rb ADDED
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('darkroom/asset')
4
+ require('darkroom/darkroom')
5
+ require('darkroom/version')
6
+
7
+ require('darkroom/errors/asset_not_found_error')
8
+ require('darkroom/errors/duplicate_asset_error')
9
+ require('darkroom/errors/missing_library_error')
10
+ require('darkroom/errors/processing_error')
11
+ require('darkroom/errors/spec_not_defined_error')
12
+
13
+ Darkroom::Asset.add_spec('.css', 'text/css',
14
+ dependency_regex: /^ *@import +(?<quote>['"]) *(?<path>.*) *\g<quote> *; *$/,
15
+ minify: -> (content) { CSSminify.compress(content) },
16
+ minify_lib: 'cssminify',
17
+ )
18
+
19
+ Darkroom::Asset.add_spec('.js', 'application/javascript',
20
+ dependency_regex: /^ *import +(?<quote>['"])(?<path>.*)\g<quote> *;? *$/,
21
+ minify: -> (content) { Uglifier.compile(content, harmony: true) },
22
+ minify_lib: 'uglifier',
23
+ )
24
+
25
+ Darkroom::Asset.add_spec('.htx', 'application/javascript',
26
+ compile: -> (path, content) { HTX.compile(path, content) },
27
+ compile_lib: 'htx',
28
+ minify: Darkroom::Asset.spec('.js').minify,
29
+ minify_lib: Darkroom::Asset.spec('.js').minify_lib,
30
+ )
31
+
32
+ Darkroom::Asset.add_spec('.html', '.html', 'text/html')
33
+ Darkroom::Asset.add_spec('.ico', 'image/x-icon')
34
+ Darkroom::Asset.add_spec('.jpg', '.jpeg', 'image/jpeg')
35
+ Darkroom::Asset.add_spec('.png', 'image/png')
36
+ Darkroom::Asset.add_spec('.svg', 'image/svg+xml')
37
+ Darkroom::Asset.add_spec('.txt', 'text/plain')
38
+ Darkroom::Asset.add_spec('.woff', 'font/woff')
39
+ Darkroom::Asset.add_spec('.woff2', 'font/woff2')
@@ -0,0 +1,300 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('digest')
4
+
5
+ class Darkroom
6
+ ##
7
+ # Represents an asset.
8
+ #
9
+ class Asset
10
+ DEPENDENCY_JOINER = "\n"
11
+ EXTENSION_REGEX = /(?=\.\w+)/.freeze
12
+
13
+ @@specs = {}
14
+
15
+ attr_reader(:content, :error, :errors, :path, :path_versioned)
16
+
17
+ ##
18
+ # Holds information about how to handle a particular asset type.
19
+ #
20
+ # * +content_type+ - HTTP MIME type string.
21
+ # * +dependency_regex+ - Regex to match lines of the file against to find dependencies. Must contain a
22
+ # named component called 'path' (e.g. +/^import (?<path>.*)/+).
23
+ # * +compile+ - Proc to call that will produce the compiled version of the asset's content.
24
+ # * +compile_lib+ - Name of a library to +require+ that is needed by the +compile+ proc.
25
+ # * +minify+ - Proc to call that will produce the minified version of the asset's content.
26
+ # * +minify_lib+ - Name of a library to +require+ that is needed by the +minify+ proc.
27
+ #
28
+ Spec = Struct.new(:content_type, :dependency_regex, :compile, :compile_lib, :minify, :minify_lib)
29
+
30
+ ##
31
+ # Defines an asset spec.
32
+ #
33
+ # * +extensions+ - File extensions to associate with this spec.
34
+ # * +content_type+ - HTTP MIME type string.
35
+ # * +other+ - Optional components of the spec (see Spec struct).
36
+ #
37
+ def self.add_spec(*extensions, content_type, **other)
38
+ spec = Spec.new(
39
+ content_type.freeze,
40
+ other[:dependency_regex].freeze,
41
+ other[:compile].freeze,
42
+ other[:compile_lib].freeze,
43
+ other[:minify].freeze,
44
+ other[:minify_lib].freeze,
45
+ ).freeze
46
+
47
+ extensions.each do |extension|
48
+ @@specs[extension] = spec
49
+ end
50
+
51
+ spec
52
+ end
53
+
54
+ ##
55
+ # Returns the spec associated with a file extension.
56
+ #
57
+ # * +extension+ - File extension of the desired spec.
58
+ #
59
+ def self.spec(extension)
60
+ @@specs[extension]
61
+ end
62
+
63
+ ##
64
+ # Returns an array of file extensions for which specs exist.
65
+ #
66
+ def self.extensions
67
+ @@specs.keys
68
+ end
69
+
70
+ ##
71
+ # Creates a new instance.
72
+ #
73
+ # * +file+ - The path to the file on disk.
74
+ # * +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
+ # * +minify+ - Boolean specifying whether or not the asset should be minified when processed.
77
+ # * +internal+ - Boolean indicating whether or not the asset is only accessible internally (i.e. as a
78
+ # dependency).
79
+ #
80
+ def initialize(path, file, manifest, minify: false, internal: false)
81
+ @path = path
82
+ @file = file
83
+ @manifest = manifest
84
+ @minify = minify
85
+ @internal = internal
86
+
87
+ @extension = File.extname(@path).downcase
88
+ @spec = self.class.spec(@extension) or raise(SpecNotDefinedError.new(@extension, @file))
89
+
90
+ require_libs
91
+ clear
92
+ end
93
+
94
+ ##
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
+ #
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
114
+
115
+ clear
116
+ read(key)
117
+ compile
118
+ minify
119
+
120
+ @fingerprint = Digest::MD5.hexdigest(@content)
121
+ @path_versioned = @path.sub(EXTENSION_REGEX, "-#{@fingerprint}")
122
+
123
+ @modified
124
+ ensure
125
+ @error = @errors.empty? ? nil : ProcessingError.new(@errors)
126
+ end
127
+
128
+ ##
129
+ # Returns the HTTP MIME type string.
130
+ #
131
+ def content_type
132
+ @spec.content_type
133
+ end
134
+
135
+ ##
136
+ # Returns appropriate HTTP headers.
137
+ #
138
+ # * +versioned+ - Uses Cache-Control header with max-age if +true+ and ETag header if +false+.
139
+ #
140
+ def headers(versioned: true)
141
+ {
142
+ 'Content-Type' => content_type,
143
+ 'Cache-Control' => ('public, max-age=31536000' if versioned),
144
+ 'ETag' => ("\"#{@fingerprint}\"" if !versioned),
145
+ }.compact!
146
+ end
147
+
148
+ ##
149
+ # Returns boolean indicating whether or not the asset is marked as internal.
150
+ #
151
+ def internal?
152
+ @internal
153
+ end
154
+
155
+ ##
156
+ # Returns boolean indicating whether or not an error was encountered the last time the asset was
157
+ # processed.
158
+ #
159
+ def error?
160
+ !!@error
161
+ end
162
+
163
+ ##
164
+ # Returns high-level object info string.
165
+ #
166
+ def inspect
167
+ "#<#{self.class}: "\
168
+ "@errors=#{@errors.inspect}, "\
169
+ "@extension=#{@extension.inspect}, "\
170
+ "@file=#{@file.inspect}, "\
171
+ "@fingerprint=#{@fingerprint.inspect}, "\
172
+ "@internal=#{@internal.inspect}, "\
173
+ "@minify=#{@minify.inspect}, "\
174
+ "@mtime=#{@mtime.inspect}, "\
175
+ "@path=#{@path.inspect}, "\
176
+ "@path_versioned=#{@path_versioned.inspect}"\
177
+ '>'
178
+ end
179
+
180
+ protected
181
+
182
+ ##
183
+ # Returns the processed content of the asset without dependencies concatenated.
184
+ #
185
+ def own_content
186
+ @own_content
187
+ end
188
+
189
+ ##
190
+ # Returns an array of all the asset's dependencies.
191
+ #
192
+ def dependencies
193
+ @dependencies
194
+ end
195
+
196
+ private
197
+
198
+ ##
199
+ # Clears content, dependencies, and errors so asset is ready for (re)processing.
200
+ #
201
+ def clear
202
+ (@errors ||= []).clear
203
+ (@dependencies ||= []).clear
204
+ (@content ||= +'').clear
205
+ (@own_content ||= +'').clear
206
+ end
207
+
208
+ ##
209
+ # Reads the asset file, building dependency array if dependencies are supported for the asset's type.
210
+ #
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
224
+ else
225
+ @own_content << line
226
+ end
227
+ 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
+ end
244
+ end
245
+
246
+ ##
247
+ # Compiles the asset if compilation is supported for the asset's type.
248
+ #
249
+ def compile
250
+ if @spec.compile
251
+ begin
252
+ @own_content = @spec.compile.call(@path, @own_content)
253
+ rescue => e
254
+ @errors << e
255
+ end
256
+ end
257
+
258
+ @content << @own_content
259
+ end
260
+
261
+ ##
262
+ # Minifies the asset if minification is supported for the asset's type, asset is marked as minifiable
263
+ # (i.e. it's not already minified), and the asset is not marked as internal-only.
264
+ #
265
+ def minify
266
+ if @spec.minify && @minify && !@internal
267
+ begin
268
+ @content = @spec.minify.call(@content)
269
+ rescue => e
270
+ @errors << e
271
+ end
272
+ end
273
+
274
+ @content
275
+ end
276
+
277
+ ##
278
+ # Requires any libraries necessary for compiling and minifying the asset based on its type. Raises a
279
+ # MissingLibraryError if library cannot be loaded.
280
+ #
281
+ # Darkroom does not explicitly depend on any libraries necessary for asset compilation or minification,
282
+ # since not every app will use every kind of asset or use minification. It is instead up to each app
283
+ # using Darkroom to specify any needed compilation and minification libraries as direct dependencies
284
+ # (e.g. specify +gem('uglifier')+ in the app's Gemfile if JavaScript minification is desired).
285
+ #
286
+ def require_libs
287
+ begin
288
+ require(@spec.compile_lib) if @spec.compile_lib
289
+ rescue LoadError
290
+ raise(MissingLibraryError.new(@spec.compile_lib, 'compile', @extension))
291
+ end
292
+
293
+ begin
294
+ require(@spec.minify_lib) if @spec.minify_lib && @minify
295
+ rescue LoadError
296
+ raise(MissingLibraryError.new(@spec.minify_lib, 'minify', @extension))
297
+ end
298
+ end
299
+ end
300
+ end
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('set')
4
+
5
+ ##
6
+ # Main class providing fast, lightweight, and straightforward web asset management.
7
+ #
8
+ class Darkroom
9
+ DEFAULT_INTERNAL_PATTERN = nil
10
+ DEFAULT_MINIFIED_PATTERN = /(\.|-)min\.\w+$/.freeze
11
+ PRISTINE = Set.new(%w[/favicon.ico /mask-icon.svg /humans.txt /robots.txt]).freeze
12
+ MIN_PROCESS_INTERVAL = 0.5
13
+
14
+ TRAILING_SLASHES = /\/+$/.freeze
15
+
16
+ attr_reader(:error, :errors)
17
+
18
+ ##
19
+ # Creates a new instance.
20
+ #
21
+ # * +load_paths+ - Path(s) where assets are located on disk.
22
+ # * +host+ - Host(s) to prepend to paths (useful when serving from a CDN in production). If multiple hosts
23
+ # are specified, they will be round-robined within each thread for each call to +#asset_path+.
24
+ # * +hosts+ - Alias of +host+ parameter.
25
+ # * +prefix+ - Prefix to prepend to asset paths (e.g. +/assets+).
26
+ # * +pristine+ - Path(s) that should not include prefix and for which unversioned form should be provided
27
+ # by default (e.g. +/favicon.ico+).
28
+ # * +minify+ - Boolean specifying whether or not to minify assets.
29
+ # * +minified_pattern+ - Regex used against asset paths to determine if they are already minified and
30
+ # should therefore be skipped over for minification.
31
+ # * +internal_pattern+ - Regex used against asset paths to determine if they should be marked as internal
32
+ # and therefore made inaccessible externally.
33
+ # * +min_process_interval+ - Minimum time required between one run of asset processing and another.
34
+ #
35
+ def initialize(*load_paths, host: nil, hosts: nil, prefix: nil, pristine: nil, minify: false,
36
+ minified_pattern: DEFAULT_MINIFIED_PATTERN, internal_pattern: DEFAULT_INTERNAL_PATTERN,
37
+ min_process_interval: MIN_PROCESS_INTERVAL)
38
+ @globs = load_paths.each_with_object({}) do |path, globs|
39
+ globs[path.chomp('/')] = File.join(path, '**', "*{#{Asset.extensions.join(',')}}")
40
+ end
41
+
42
+ @hosts = (Array(host) + Array(hosts)).map! { |host| host.sub(TRAILING_SLASHES, '') }
43
+ @minify = minify
44
+ @internal_pattern = internal_pattern
45
+ @minified_pattern = minified_pattern
46
+
47
+ @prefix = prefix&.sub(TRAILING_SLASHES, '')
48
+ @prefix = nil if @prefix && @prefix.empty?
49
+
50
+ @pristine = PRISTINE.dup.merge(Array(pristine))
51
+
52
+ @min_process_interval = min_process_interval
53
+ @last_processed_at = 0
54
+ @mutex = Mutex.new
55
+ @manifest = {}
56
+
57
+ Thread.current[:darkroom_host_index] = -1 unless @hosts.empty?
58
+ end
59
+
60
+ ##
61
+ # Walks all load paths and refreshes any assets that have been modified on disk since the last call to
62
+ # this method.
63
+ #
64
+ def process
65
+ return if Time.now.to_f - @last_processed_at < @min_process_interval
66
+
67
+ if @mutex.locked?
68
+ @mutex.synchronize {}
69
+ return
70
+ end
71
+
72
+ @mutex.synchronize do
73
+ @errors = []
74
+ found = {}
75
+
76
+ @globs.each do |load_path, glob|
77
+ Dir.glob(glob).sort.each do |file|
78
+ path = file.sub(load_path, '')
79
+
80
+ if found.key?(path)
81
+ @errors << DuplicateAssetError.new(path, found[path], load_path)
82
+ else
83
+ found[path] = load_path
84
+
85
+ @manifest[path] ||= Asset.new(path, file, @manifest,
86
+ internal: internal = @internal_pattern && path =~ @internal_pattern,
87
+ minify: @minify && !internal && path !~ @minified_pattern,
88
+ )
89
+ end
90
+ end
91
+ end
92
+
93
+ @manifest.select! { |path, _| found.key?(path) }
94
+
95
+ found.each do |path, _|
96
+ @manifest[path].process(@last_processed_at)
97
+ @manifest[@manifest[path].path_versioned] = @manifest[path]
98
+
99
+ @errors += @manifest[path].errors
100
+ end
101
+ ensure
102
+ @last_processed_at = Time.now.to_f
103
+ @error = @errors.empty? ? nil : ProcessingError.new(@errors)
104
+ end
105
+ end
106
+
107
+ ##
108
+ # Does the same thing as #process, but raises an exception if any errors were encountered.
109
+ #
110
+ def process!
111
+ process
112
+
113
+ raise(@error) if @error
114
+ end
115
+
116
+ ##
117
+ # Returns boolean indicating whether or not there were any errors encountered the last time assets were
118
+ # processed.
119
+ #
120
+ def error?
121
+ !!@error
122
+ end
123
+
124
+ ##
125
+ # Returns an Asset object, given its external path. An external path includes any prefix and and can be
126
+ # either the versioned or unversioned form of the asset path (i.e. how an HTTP request for the asset comes
127
+ # in). For example, to get the Asset object with path +/js/app.js+ when prefix is +/assets+:
128
+ #
129
+ # darkroom.asset('/assets/js/app.<hash>.js')
130
+ # darkroom.asset('/assets/js/app.js')
131
+ #
132
+ # * +path+ - The external path of the asset.
133
+ #
134
+ 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
143
+ end
144
+
145
+ ##
146
+ # Returns the external asset path, given its internal path. An external path includes any prefix and and
147
+ # can be either the versioned or unversioned form of the asset path (i.e. how an HTTP request for the
148
+ # asset comes in). For example, to get the external path for the Asset object with path +/js/app.js+ when
149
+ # prefix is +/assets+:
150
+ #
151
+ # darkroom.asset_path('/js/app.js') # => /assets/js/app.<hash>.js
152
+ # darkroom.asset_path('/js/app.js', versioned: false) # => /assets/js/app.js
153
+ #
154
+ # * +path+ - The internal path of the asset.
155
+ # * +versioned+ - Boolean indicating whether the versioned or unversioned path should be returned.
156
+ #
157
+ def asset_path(path, versioned: !@pristine.include?(path))
158
+ asset = @manifest[path] or return nil
159
+
160
+ host = @hosts.empty? ? '' : @hosts[
161
+ Thread.current[:darkroom_host_index] = (Thread.current[:darkroom_host_index] + 1) % @hosts.size
162
+ ]
163
+ prefix = @prefix unless @pristine.include?(path)
164
+
165
+ "#{host}#{prefix}#{versioned ? asset.path_versioned : path}"
166
+ end
167
+
168
+ ##
169
+ # Calls #asset_path and raises a AssetNotFoundError if the asset doesn't exist (instead of just returning
170
+ # nil).
171
+ #
172
+ # * +versioned+ - Boolean indicating whether the versioned or unversioned path should be returned.
173
+ #
174
+ def asset_path!(path, versioned: true)
175
+ asset_path(path, versioned: versioned) or raise(AssetNotFoundError.new(path))
176
+ end
177
+
178
+ ##
179
+ # Writes assets to disk. This is useful when deploying to a production environment where assets will be
180
+ # uploaded to and served from a CDN or proxy server.
181
+ #
182
+ # * +dir+ - Directory to write the assets to.
183
+ # * +clear+ - Boolean indicating whether or not the existing contents of the directory should be deleted
184
+ # before performing the dump.
185
+ # * +include_pristine+ - Boolean indicating whether or not to include pristine assets (when dumping for
186
+ # the purpose of uploading to a CDN, assets such as /robots.txt and /favicon.ico don't need to be
187
+ # included).
188
+ #
189
+ def dump(dir, clear: false, include_pristine: true)
190
+ dir = File.expand_path(dir)
191
+ written = Set.new
192
+
193
+ FileUtils.mkdir_p(dir)
194
+ Dir.each_child(dir) { |child| FileUtils.rm_rf(File.join(dir, child)) } if clear
195
+
196
+ @manifest.each do |_, asset|
197
+ next if asset.internal?
198
+ next if written.include?(asset.path)
199
+ next if @pristine.include?(asset.path) && !include_pristine
200
+
201
+ external_path = asset_path(asset.path)
202
+ file_path = File.join(dir, external_path)
203
+
204
+ FileUtils.mkdir_p(File.dirname(file_path))
205
+ File.write(file_path, asset.content)
206
+
207
+ written << asset.path
208
+ end
209
+ end
210
+
211
+ ##
212
+ # Returns high-level object info string.
213
+ #
214
+ def inspect
215
+ "#<#{self.class}: "\
216
+ "@errors=#{@errors.inspect}, "\
217
+ "@globs=#{@globs.inspect}, "\
218
+ "@hosts=#{@hosts.inspect}, "\
219
+ "@internal_pattern=#{@internal_pattern.inspect}, "\
220
+ "@last_processed_at=#{@last_processed_at.inspect}, "\
221
+ "@min_process_interval=#{@min_process_interval.inspect}, "\
222
+ "@minified_pattern=#{@minified_pattern.inspect}, "\
223
+ "@minify=#{@minify.inspect}, "\
224
+ "@prefix=#{@prefix.inspect}, "\
225
+ "@pristine=#{@pristine.inspect}"\
226
+ '>'
227
+ end
228
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Darkroom
4
+ ##
5
+ # Error class used when an asset requested explicitly or specified as a dependency of another cannot be
6
+ # found.
7
+ #
8
+ class AssetNotFoundError < StandardError
9
+ attr_reader(:path, :referenced_from, :referenced_from_line)
10
+
11
+ ##
12
+ # Creates a new instance.
13
+ #
14
+ # * +path+ - The path of the asset that cannot be found.
15
+ # * +referenced_from+ - The path of the asset the not-found asset was referenced from.
16
+ # * +referenced_from_line+ - The line number where the not-found asset was referenced.
17
+ #
18
+ def initialize(path, referenced_from = nil, referenced_from_line = nil)
19
+ @path = path
20
+ @referenced_from = referenced_from
21
+ @referenced_from_line = referenced_from_line
22
+ end
23
+
24
+ ##
25
+ # Returns a string representation of the error.
26
+ #
27
+ def to_s
28
+ "Asset not found#{
29
+ " (referenced from #{@referenced_from}:#{@referenced_from_line || '?'})" if @referenced_from
30
+ }: #{@path}"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Darkroom
4
+ ##
5
+ # Error class used when an asset exists in multiple load paths.
6
+ #
7
+ class DuplicateAssetError < StandardError
8
+ attr_reader(:path, :first_load_path, :second_load_path)
9
+
10
+ ##
11
+ # Creates a new instance.
12
+ #
13
+ # * +path+ - The path of the asset that has the same path as another asset.
14
+ # * +first_load_path+ - The load path where the first asset with the path was found.
15
+ # * +second_load_path+ - The load path where the second asset with the path was found.
16
+ #
17
+ def initialize(path, first_load_path, second_load_path)
18
+ @path = path
19
+ @first_load_path = first_load_path
20
+ @second_load_path = second_load_path
21
+ end
22
+
23
+ ##
24
+ # Returns a string representation of the error.
25
+ #
26
+ def to_s
27
+ "Asset file exists in both #{@first_load_path} and #{@second_load_path}: #{@path}"
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Darkroom
4
+ ##
5
+ # Error class used when a needed library cannot be loaded. See Asset#require_libs.
6
+ #
7
+ class MissingLibraryError < StandardError
8
+ attr_reader(:library, :need, :extension)
9
+
10
+ ##
11
+ # Creates a new instance.
12
+ #
13
+ # * +library+ - The name of the library that's missing.
14
+ # * +need+ - The reason the library is needed ('compile' or 'minify').
15
+ # * +extension+ - The extenion of the type of asset that needs the library.
16
+ #
17
+ def initialize(library, need, extension)
18
+ @library = library
19
+ @need = need
20
+ @extension = extension
21
+ end
22
+
23
+ ##
24
+ # Returns a string representation of the error.
25
+ #
26
+ def to_s
27
+ "Cannot #{@need} #{@extension} file(s): #{@library} library not available"
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Darkroom
4
+ ##
5
+ # Error class used to wrap all accumulated errors encountered during asset processing.
6
+ #
7
+ class ProcessingError < StandardError
8
+ ##
9
+ # Creates a new instance.
10
+ #
11
+ # * +errors+ - Error or array of errors.
12
+ #
13
+ def initialize(errors)
14
+ @errors = Array(errors)
15
+ end
16
+
17
+ ##
18
+ # Returns a string representation of the error.
19
+ #
20
+ def to_s
21
+ "Errors were encountered while processing assets:\n #{@errors.map(&:to_s).join("\n ")}"
22
+ end
23
+
24
+ ##
25
+ # Passes any missing method call on to the @errors array.
26
+ #
27
+ def method_missing(m, *args, &block)
28
+ @errors.send(m, *args, &block)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Darkroom
4
+ ##
5
+ # Error class used when a spec is not defined for a particular file extension.
6
+ #
7
+ class SpecNotDefinedError < StandardError
8
+ attr_reader(:extension, :file)
9
+
10
+ ##
11
+ # Creates a new instance.
12
+ #
13
+ # * +extension+ - Extension for which there is no spec defined.
14
+ # * +file+ - File path of the asset whose loading was attempted.
15
+ #
16
+ def initialize(extension, file = nil)
17
+ @extension = extension
18
+ @file = file
19
+ end
20
+
21
+ ##
22
+ # Returns a string representation of the error.
23
+ #
24
+ def to_s
25
+ "Spec not defined for #{@extension} files#{" (#{@file})" if @file}"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Darkroom
4
+ VERSION = '0.0.1'
5
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: darkroom
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Nate Pickens
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-02-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 5.11.2
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: 6.0.0
37
+ type: :development
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 5.11.2
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: 6.0.0
47
+ description: Darkroom provides simple web asset management complete with dependency
48
+ bundling based on import statements, compilation, and minification.
49
+ email:
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - LICENSE
55
+ - README.md
56
+ - VERSION
57
+ - lib/darkroom.rb
58
+ - lib/darkroom/asset.rb
59
+ - lib/darkroom/darkroom.rb
60
+ - lib/darkroom/errors/asset_not_found_error.rb
61
+ - lib/darkroom/errors/duplicate_asset_error.rb
62
+ - lib/darkroom/errors/missing_library_error.rb
63
+ - lib/darkroom/errors/processing_error.rb
64
+ - lib/darkroom/errors/spec_not_defined_error.rb
65
+ - lib/darkroom/version.rb
66
+ homepage: https://github.com/npickens/darkroom
67
+ licenses:
68
+ - MIT
69
+ metadata:
70
+ allowed_push_host: https://rubygems.org
71
+ homepage_uri: https://github.com/npickens/darkroom
72
+ source_code_uri: https://github.com/npickens/darkroom
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 2.5.8
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubygems_version: 3.2.3
89
+ signing_key:
90
+ specification_version: 4
91
+ summary: A fast, lightweight, and straightforward web asset management library.
92
+ test_files: []