darkroom 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []