darkroom 0.0.8 → 0.0.10

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.
data/README.md CHANGED
@@ -1,28 +1,24 @@
1
1
  # Darkroom
2
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
- The following file types are supported out of the box, though support for others can be added (see the
10
- [Extending](#extending) section):
11
-
12
- | Name | Content Type | Extension(s) |
13
- |------------|------------------|--------------|
14
- | CSS | text/css | .css |
15
- | HTML | text/html | .htm, .html |
16
- | HTX | text/javascript | .htx |
17
- | ICO | image/x-icon | .ico |
18
- | JavaScript | text/javascript | .js |
19
- | JPEG | image/jpeg | .jpg, .jpeg |
20
- | JSON | application/json | .json |
21
- | PNG | image/png | .png |
22
- | SVG | image/svg+xml | .svg |
23
- | Text | text/plain | .txt |
24
- | WOFF | font/woff | .woff |
25
- | WOFF2 | font/woff2 | .woff2 |
3
+ Darkroom is a simple and straightforward web asset management and bundling library written entirely in Ruby.
4
+ It is designed to be used directly within a Ruby web server process—no external dependencies or process
5
+ management is needed.
6
+
7
+ * **Asset Bundling** — CSS and JavaScript assets are bundled using each language's native import statement
8
+ syntax.
9
+ * `@import '/header.css';` includes the contents of header.css in a CSS asset.
10
+ * `import '/api.js'` includes the contents of api.js in a JavaScript asset.
11
+ * **Asset References** — Asset paths are versioned with a URL query parameter.
12
+ * `<img src='/logo.svg?asset-path'>` gets replaced with `<img src='/logo-[fingerprint].svg'>`.
13
+ * **Asset Inlining** — Certain kinds of assets (e.g. images in HTML documents) can be inlined using a URL
14
+ query parameter.
15
+ * `<img src='/logo.svg?asset-content=utf8'>` gets replaced with
16
+ `<img src='data:image/svg+xml;utf8,<svg>[...]</svg>'>`.
17
+ * **JavaScript Modules** — JavaScript bundles can (optionally) encapsulate the content of each file with
18
+ imports defined as local variables to mimic native ES6 modules within a single file.
19
+ * **In-Memory** Processed assets are all stored in and served directly from memory to avoid the issues
20
+ that generally ensue from writing to and managing files on disk. Assets can however be dumped to disk for
21
+ upload to a CDN or proxy server in production environments.
26
22
 
27
23
  ## Installation
28
24
 
@@ -55,21 +51,26 @@ To create and start using a Darkroom instance, specify one or more load paths (a
55
51
  optional):
56
52
 
57
53
  ```ruby
54
+ Darkroom.javascript_iife = true # Use IIFEs to emulate ES6-style JavaScript modules
55
+
58
56
  darkroom = Darkroom.new('app/assets', 'vendor/assets', '...',
59
- hosts: [ # Hosts to prepend to asset paths (useful in production
60
- 'https://cname1.cdn.com', # when assets are served from a CDN with multiple
61
- 'https://cname2.cdn.com', # cnames); hosts are chosen round-robin per thread
57
+ hosts: [ # Hosts to prepend to asset paths (useful in production
58
+ 'https://cname1.cdn.com', # when assets are served from a CDN with multiple
59
+ 'https://cname2.cdn.com', # cnames); hosts are chosen round-robin per thread
62
60
  '...',
63
61
  ],
64
- prefix: '/static', # Prefix to add to all asset paths
65
- pristine: ['/google-verify.html'], # Paths with no prefix or versioning (/favicon.ico,
66
- # /mask-icon.svg, /humans.txt, and /robots.txt are
67
- # included automatically)
68
- minify: true, # Minify assets that can be minified
69
- minified_pattern: /(\.|-)min\.\w+$/, # Files to skip minification on when minify: true
70
- internal_pattern: /^\/components\//, # Files to disallow direct external access to (they can
71
- # still be imported into other assets)
72
- min_process_interval: 1, # Minimum time that must elapse between process calls
62
+ prefix: '/static', # Prefix to add to all asset paths
63
+ pristine: ['/google-verify.html'], # Paths with no prefix or versioning (assets such as
64
+ # /favicon.ico and /robots.txt are included
65
+ # automatically)
66
+ entries: /^\/controllers\//, # Assets that will be directly accessed (fewer means
67
+ # better performance); can be a string, regex, or
68
+ # array of such
69
+ minify: true, # Minify assets that can be minified
70
+ minified: /(\.|-)min\.\w+$/, # Files to skip minification on when minify: true; can
71
+ # be a string, regex, or array of such
72
+ min_process_interval: 1, # Minimum seconds that must elapse between process
73
+ # calls (otherwise processing is skipped)
73
74
  )
74
75
  ```
75
76
 
@@ -98,86 +99,158 @@ To work with assets:
98
99
 
99
100
  ```ruby
100
101
  # A Darkroom instance has a few convenience helper methods.
101
- path = darkroom.asset_path('/js/app.js') # => '/static/js/app-[fingerprint].js'
102
- integrity = darkroom.asset_integrity('/js/app.js') # => 'sha384-[hash]'
102
+ path = darkroom.asset_path('/js/app.js') # => "/static/js/app-[fingerprint].js"
103
+ integrity = darkroom.asset_integrity('/js/app.js') # => "sha384-[hash]"
103
104
 
104
105
  # Retrieve the Asset object associated with a path.
105
106
  asset = darkroom.asset(path)
106
107
 
107
- # Prefix (if set on the Darkroom instance) is included in the unversioned and versioned paths.
108
- assest.path # => '/js/app.js'
109
- assest.path_unversioned # => '/static/js/app.js'
110
- assest.path_versioned # => '/static/js/app-[fingerprint].js'
108
+ # Prefix (if set on the Darkroom instance) is included in the unversioned and versioned
109
+ # paths.
110
+ assest.path # => "/js/app.js"
111
+ assest.path_unversioned # => "/static/js/app.js"
112
+ assest.path_versioned # => "/static/js/app-[fingerprint].js"
111
113
 
112
- asset.content_type # => 'text/javascript'
113
114
  asset.content # Content of processed /js/app.js file
114
115
 
115
- asset.headers # => {'Content-Type' => 'text/javascript',
116
- # 'Cache-Control' => 'public, max-age=31536000'}
117
- asset.headers(versioned: false) # => {'Content-Type' => 'text/javascript',
118
- # 'ETag' => '[fingerprint]'}
119
-
120
- asset.integrity # => 'sha384-[hash]'
121
- asset.integrity(:sha256) # => 'sha256-[hash]'
122
- asset.integrity(:sha512) # => 'sha512-[hash]'
116
+ asset.content_type # => "text/javascript"
117
+ asset.binary? # => false
118
+ asset.font? # => false
119
+ asset.image? # => false
120
+ asset.entry? # => true
121
+
122
+ asset.error? # => true
123
+ asset.errors # => [#<Darkroom::AssetError ...>, ...]
124
+
125
+ asset.fingerprint # => "[MD5 hash of asset content]"
126
+ asset.headers # => {"Content-Type" => "text/javascript",
127
+ # "Cache-Control" => "public, max-age=31536000"}
128
+ asset.headers(versioned: false) # => {"Content-Type" => "text/javascript",
129
+ # "ETag" => "[fingerprint]"}
130
+
131
+ asset.integrity # => "sha384-[hash]"
132
+ asset.integrity(:sha256) # => "sha256-[hash]"
133
+ asset.integrity(:sha384) # => "sha384-[hash]"
134
+ asset.integrity(:sha512) # => "sha512-[hash]"
123
135
  ```
124
136
 
125
137
  ## Asset Bundling
126
138
 
127
- CSS and JavaScript assets specify their dependencies by way of each language's native import statement. Each
128
- import statement is replaced with content of the referenced asset. Example:
129
-
130
- ```javascript
131
- // Unprocessed /api.js
132
- function api() {
133
- console.log('API called!')
134
- }
135
-
136
- // Unprocessed /app.js
137
- import '/api.js'
138
-
139
- api()
140
-
141
- // Processed /app.js
142
- function api() {
143
- console.log('API called!')
144
- }
145
-
146
-
147
- api()
148
- ```
149
-
150
- The same applies for CSS files. Example:
151
-
152
- ```css
153
- /* Unprocessed /header.css */
154
- header {
155
- background: #f1f1f1;
156
- }
157
-
158
- /* Unprocessed /app.css */
159
- @import '/header.css';
160
-
161
- body {
162
- background: #fff;
163
- }
164
-
165
- /* Processed /app.css */
166
- header {
167
- background: #f1f1f1;
168
- }
169
-
170
-
171
- body {
172
- background: #fff;
173
- }
174
- ```
139
+ JavaScript and CSS assets specify their dependencies by way of each language's native import statement. Each
140
+ import statement is replaced with the content of the imported asset.
175
141
 
176
142
  Imported assets can also contain import statements, and those assets are all included in the base asset.
177
143
  Imports can even be cyclical. If `asset-a.css` imports `asset-b.css` and vice-versa, each asset will simply
178
144
  contain the content of both of those assets (though order will be different as an asset's own content always
179
145
  comes after any imported assets' contents).
180
146
 
147
+ ### JavaScript - Single Scope
148
+
149
+ By default, JavaScript bundles are a simple concatenation of files. Imports should all be "side effect"
150
+ style and result in all code using one shared scope:
151
+
152
+ * **/api.js** (raw file)
153
+ ```javascript
154
+ function API() {
155
+ console.log('API called!')
156
+ }
157
+ ```
158
+ * **/app.js** (raw file)
159
+ ```javascript
160
+ import '/api.js'
161
+
162
+ API()
163
+ ```
164
+ * **/app.js** (processed)
165
+ ```javascript
166
+ function API() {
167
+ console.log('API called!')
168
+ }
169
+
170
+ API()
171
+ ```
172
+
173
+ ### JavaScript - Modules
174
+
175
+ Alternatively, setting `Darkroom.javascript_iife = true` will cause JavaScript assets to be compiled to a
176
+ series of IIFEs that provide the same encapsulation as native ES6 modules. In this case, objects must be
177
+ explicitly exported to be importable and named (rather than side effect) imports used:
178
+
179
+ * **/api.js** (raw file)
180
+ ```javascript
181
+ export function API() {
182
+ console.log('API called!')
183
+ }
184
+ ```
185
+ * **/app.js** (raw file)
186
+ ```javascript
187
+ import {API} from '/api.js'
188
+
189
+ API()
190
+ ```
191
+ * **/app.js** (processed)
192
+ ```javascript
193
+ ((...bundle) => {
194
+ const modules = {}
195
+ const setters = []
196
+ const $import = (name, setter) =>
197
+ modules[name] ? setter(modules[name]) : setters.push([setter, name])
198
+
199
+ for (const [name, def] of bundle)
200
+ modules[name] = def($import)
201
+
202
+ for (const [setter, name] of setters)
203
+ setter(modules[name])
204
+ })(
205
+ ['/api.js', $import => {
206
+ function API() {
207
+ console.log('API called!')
208
+ }
209
+
210
+ return Object.seal({
211
+ API: API,
212
+ })
213
+ }],
214
+
215
+ ['/app.js', $import => {
216
+ let API; $import('/api.js', m => API = m.API)
217
+
218
+ API()
219
+
220
+ return Object.seal({})
221
+ }],
222
+ )
223
+ ```
224
+
225
+ ### CSS
226
+
227
+ CSS imports are always just a simple concatenation.
228
+
229
+ * **/header.css** (raw file)
230
+ ```css
231
+ header {
232
+ background: #f1f1f1;
233
+ }
234
+ ```
235
+ * **/app.css** (raw file)
236
+ ```css
237
+ @import '/header.css';
238
+
239
+ body {
240
+ background: #fff;
241
+ }
242
+ ```
243
+ * **/app.css** (processed)
244
+ ```css
245
+ header {
246
+ background: #f1f1f1;
247
+ }
248
+
249
+ body {
250
+ background: #fff;
251
+ }
252
+ ```
253
+
181
254
  ## Asset References
182
255
 
183
256
  Asset paths and content can be inserted into an asset by referencing an asset's path and including a query
@@ -212,9 +285,11 @@ replaced appropriately.
212
285
  </head>
213
286
 
214
287
  <body>
215
- <img src='/logo.svg?asset-content-displace'>
288
+ <img src='/logo.svg?asset-content=displace'>
216
289
  </body>
290
+ ```
217
291
 
292
+ ```html
218
293
  <!-- Result -->
219
294
  <head>
220
295
  <title>My App</title>
@@ -229,23 +304,264 @@ replaced appropriately.
229
304
 
230
305
  ## Extending
231
306
 
232
- Darkroom is extensible. Support for arbitrary file types can be added as follows (`content_type` is required
233
- but all other keyword arguments are optional):
307
+ The following file types are supported out of the box:
308
+
309
+ | Name | Content Type | Extension(s) |
310
+ |------------|------------------|--------------|
311
+ | APNG | image/apng | .apng |
312
+ | AVIF | image/avif | .avif |
313
+ | CSS | text/css | .css |
314
+ | GIF | image/gif | .gif |
315
+ | HTML | text/html | .htm, .html |
316
+ | HTX | text/javascript | .htx |
317
+ | ICO | image/x-icon | .ico |
318
+ | JavaScript | text/javascript | .js |
319
+ | JPEG | image/jpeg | .jpg, .jpeg |
320
+ | JSON | application/json | .json |
321
+ | PNG | image/png | .png |
322
+ | SVG | image/svg+xml | .svg |
323
+ | Text | text/plain | .txt |
324
+ | WebP | image/webp | .webp |
325
+ | WOFF | font/woff | .woff |
326
+ | WOFF2 | font/woff2 | .woff2 |
327
+
328
+ But Darkroom is extensible, allowing support for any kind of asset to be added. This is done most simply by
329
+ specifying one or more extensions and a content type:
234
330
 
235
331
  ```ruby
236
- # Simple type with no special behavior.
237
- Darkroom.register('.extension1', 'extension2', '...', 'content/type')
238
-
239
- # Complex type with special behavior.
240
- Darkroom.register('.extension1', 'extension2', '...',
241
- content_type: 'content/type', # HTTP MIME type string
242
- import_regex: /import (?<path>.*)/, # Regex for identifying imports for bundling
243
- reference_regex: /ref=(?<path>.*)/, # Regex for identifying references to other assets
244
- compile_lib: 'some-compile-lib', # Name of library required for compilation
245
- compile: ->(path, content) { '...' }, # Lambda that returns compiled content
246
- minify_lib: 'some-minify-lib', # Name of library required for minification
247
- minify: ->(content) { '...' }, # Lambda that returns minified content
248
- )
332
+ Darkroom.register('.ext1', '.ext2', '...', 'content/type')
333
+ ```
334
+
335
+ ### DSL
336
+
337
+ For more advanced functionality, a DSL is provided which can be used one of three ways. With a block:
338
+
339
+ ```ruby
340
+ Darkroom.register('.ext1', '.ext2', '...') do
341
+ # ...
342
+ end
343
+ ```
344
+
345
+ Or with a class that extends `Darkroom::Delegate`:
346
+
347
+ ```ruby
348
+ class MyDelegate < Darkroom::Delegate
349
+ # ...
350
+ end
351
+
352
+ Darkroom.register('.ext1', '.ext2', '...', MyDelegate)
353
+ ```
354
+
355
+ Or with both:
356
+
357
+ ```ruby
358
+ class MyDelegate < Darkroom::Delegate
359
+ # ...
360
+ end
361
+
362
+ Darkroom.register('.ext1', '.ext2', '...', MyDelegate) do
363
+ # Extend MyDelegate
364
+ end
365
+ ```
366
+
367
+ The DSL supports basic parsing via regular expressions, with special behavior for import statements and
368
+ references. Compilation, finalization, and minification behavior can also be configured.
369
+
370
+ #### Content Type
371
+
372
+ ```ruby
373
+ Darkroom.register('.ext1', '.ext2', '...') do
374
+ content_type('content/type') # HTTP MIME type string.
375
+
376
+ # ...
377
+ end
378
+ ```
379
+
380
+ #### Imports
381
+
382
+ Imports are references to other assets, identified via regex, which get prepended to an asset's own content.
383
+ The regex requires a named component, `path`, as it is used internally to determine the asset being imported
384
+ (leveraging `Asset::QUOTED_PATH_REGEX` within one's own regex is helpful).
385
+
386
+ A block is optional, but can be used to accumulate parse data and/or override the default behavior of
387
+ removing an import statement altogether by returning a string to replace it with.
388
+
389
+ ```ruby
390
+ Darkroom.register('.ext1', '.ext2', '...') do
391
+ # ...
392
+
393
+ # The (optional) block is passed three keyword arguments:
394
+ # parse_data: - Hash for storing arbitrary data across calls to this and other handlers.
395
+ # match: - MatchData object from the match against the regex.
396
+ # asset: - Asset object of the asset being imported.
397
+ import(/import '(?<path>[^']+)';/) do |parse_data:, match:, asset:|
398
+ parse_data[:imports] ||= [] # Accumulate and use arbitrary parse data.
399
+ parse_data[:imports] << match[:path] # Use the MatchData object of the regex match.
400
+
401
+ if asset.binary? # Access the Asset object of the imported asset.
402
+ error('Binary asset not allowed!') # Halt execution of the block and record an error.
403
+ end
404
+
405
+ # Return nil for default behavior (import statement is removed).
406
+ nil
407
+
408
+ # ...Or return a string as the replacement for the import statement.
409
+ "/* [#{asset.path}] */"
410
+ end
411
+ end
412
+ ```
413
+
414
+ #### References
415
+
416
+ References are non-import references to other assets, identified via regex, which result in either the
417
+ asset's path or content being inserted in place. The regex requires named components `quote`, `quoted`,
418
+ `path`, `entity`, and `format`, as these are used internally to determine the asset being referenced and how
419
+ it should be treated (leveraging `Asset::REFERENCE_REGEX` within one's own regex is helpful). See the [Asset
420
+ References](#asset-references) section for more detail.
421
+
422
+ * `quote` - The type of quote used (e.g. `'` or `"`)
423
+ * `quoted` - The portion of text within the `quote`s
424
+ * `path` - The path of the asset
425
+ * `entity` - Either 'path' or 'content'
426
+ * `format` - Format of the path or content
427
+ * If `entity` == 'path' - Either 'versioned' or 'unversioned'
428
+ * If `entity` == 'content' - One of 'base64', 'utf8', or 'displace'
429
+
430
+ A block is optional, but can be used to accumulate parse data and/or override the default substitution
431
+ behavior.
432
+
433
+ ```ruby
434
+ Darkroom.register('.ext1', '.ext2', '...') do
435
+ # ...
436
+
437
+ reference_regex = /ref=#{Asset::REFERENCE_REGEX.source}/x
438
+
439
+ # The (optional) block is passed four keyword arguments:
440
+ #
441
+ # parse_data: - Hash for storing arbitrary data across calls to this and other handlers.
442
+ # match: - MatchData object from the match against the regex.
443
+ # asset: - Asset object of the asset being referenced.
444
+ # format: - String format of the reference (see Asset::REFERENCE_FORMATS).
445
+ reference(reference_regex) do |parse_data:, match:, asset:, format:|
446
+ parse_data[:refs] ||= [] # Accumulate and use arbitrary parse data.
447
+ parse_data[:refs] << match[:path] # Use the MatchData object of the regex match.
448
+
449
+ if format == 'base64' # See Asset References section for format details.
450
+ error('Format must be utf8!') # Halt execution of the block and register an error.
451
+ end
452
+
453
+ # Return nil for default behavior (path or content is substituted based on format).
454
+ nil
455
+
456
+ # ...Or return a string to use in lieu of default substitution.
457
+ asset.content.gsub('#', '%23') if format == 'utf8'
458
+
459
+ # ...Or return nil or a string, a start index, and an end index of text to substitute.
460
+ ["[ref]#{asset.content.gsub('#', '%23')}[/ref]", match.begin(0), match.end(0)]
461
+ end
462
+ end
463
+ ```
464
+
465
+ #### Parsing
466
+
467
+ More generalized parsing of any asset-specific text of interest can be performed with `parse` calls, which
468
+ take a name, regex, and block that returns the substitution for the matched text.
469
+
470
+
471
+ ```ruby
472
+ Darkroom.register('.ext1', '.ext2', '...') do
473
+ # ...
474
+
475
+ # The block is passed two keyword arguments:
476
+ #
477
+ # parse_data: - Hash for storing arbitrary data across calls to this and other handlers.
478
+ # match: - MatchData object from the match against the regex.
479
+ parse(:exports, /export (?<name>.+)/) do |parse_data:, match:|
480
+ parse_data[:exports] ||= [] # Accumulate and use arbitrary parse data.
481
+ parse_data[:exports] << match[:name] # Use the MatchData object of the regex match.
482
+
483
+ # Return nil for default behavior (matched text is removed).
484
+ nil
485
+
486
+ # ...Or return a string as the replacement for the matched text.
487
+ "exports.#{match[:name]} = "
488
+
489
+ # ...Or return a string, a start index, and an end index of text to substitute.
490
+ [match[:name].upcase, match.begin(:name), match.end(:name)]
491
+ end
492
+
493
+ # Any number of parse statements are allowed and are run in the order they are declared.
494
+ parse(:something_else, /.../) do |parse_data:, match:|
495
+ # ...
496
+ end
497
+ end
498
+ end
499
+ ```
500
+
501
+ #### Compile
502
+
503
+ Compilation allows for a library to require (optional), a delegate to use after compilation (optional), and
504
+ a block that returns the compiled version of the asset's own content.
505
+
506
+ Passing a delegate will cause the asset to be treated as if it is that type once it has been compiled. For
507
+ example, HTX templates use `delegate: JavaScriptDelegate` because they get compiled to JavaScript and thus
508
+ should be treated as JavaScript files post compilation.
509
+
510
+ ```ruby
511
+ Darkroom.register('.ext1', '.ext2', '...') do
512
+ # ...
513
+
514
+ # The block is passed three keyword arguments:
515
+ #
516
+ # parse_data: - Hash for storing arbitrary data across calls to this and other handlers.
517
+ # path: - String path of the asset.
518
+ # own_content: - String own content (without imports) of the asset.
519
+ compile(lib: 'compile_lib', delegate: SomeDelegate) do |parse_data:, path:, own_content:|
520
+ CompileLib.compile(own_content)
521
+ end
522
+ end
523
+ ```
524
+
525
+ #### Finalize
526
+
527
+ Finalization happens once an asset is fully processed and compiled (though before minification). A library
528
+ can be provided to require (optional) and the block should return the finalized version of the asset's
529
+ compiled content.
530
+
531
+ ```ruby
532
+ Darkroom.register('.ext1', '.ext2', '...') do
533
+ # ...
534
+
535
+ # The block is passed three keyword arguments:
536
+ #
537
+ # parse_data: - Hash for storing arbitrary data across calls to this and other handlers.
538
+ # path: - String path of the asset.
539
+ # content: - String content of the compiled asset (with imports prepended).
540
+ finalize(lib: 'finalize_lib') do |parse_data:, path:, content:|
541
+ FinalizeLib.finalize(content)
542
+ end
543
+ end
544
+ ```
545
+
546
+ #### Minify
547
+
548
+ Minification is the very last thing that happens to an asset's content, though it will only happen if
549
+ minification is enabled on the Darkroom instance. A library can be provided to require (optional) and the
550
+ block should return the minified version of the asset's finalized content.
551
+
552
+ ```ruby
553
+ Darkroom.register('.ext1', '.ext2', '...') do
554
+ # ...
555
+
556
+ # The block is passed three keyword arguments:
557
+ #
558
+ # parse_data: - Hash for storing arbitrary data across calls to this and other handlers.
559
+ # path: - String oath of the asset being minified.
560
+ # content: - string content of the finalized asset.
561
+ minify(lib: 'minify_lib') do |parse_data:, path:, content:|
562
+ MinifyLib.compress(content)
563
+ end
564
+ end
249
565
 
250
566
  ```
251
567
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.8
1
+ 0.0.10