bun_bun_bundle 0.12.1 → 0.13.0
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 +4 -4
- data/README.md +11 -0
- data/exe/bun_bun_bundle +4 -0
- data/lib/bun/bun_bundle.js +104 -30
- data/lib/bun_bun_bundle/helpers.rb +29 -6
- data/lib/bun_bun_bundle/manifest.rb +20 -1
- data/lib/bun_bun_bundle/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 344a23b7763c7396493c708342f37c5caf1df751cac1fbbaa77b4d6e73c31aea
|
|
4
|
+
data.tar.gz: c82e810ba3503fc7f1808074ceb61cc8cb8095a6617d267692b61e9fb864780c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 14da826847e471f03ad80b4763c34e10fb9cef6b91a41bd0525bcc62eb17039bd8043bf3d9c6ed0232aa2842f2daad1f05adde5121c5b45fe6925645a7bc713d
|
|
7
|
+
data.tar.gz: 269aafe80a301d4c662e7417413a9e98d3fa5225320df0f641526ae978c1e363defb7019f0f2c67d398d48a8d85b4d4dbddfa3da0e7ee4dc356da0a23ac1bbf2
|
data/README.md
CHANGED
|
@@ -183,6 +183,9 @@ bun_bun_bundle build --fingerprint
|
|
|
183
183
|
# Strip sourcemaps from a prod build
|
|
184
184
|
bun_bun_bundle build --prod --sourcemap=none
|
|
185
185
|
|
|
186
|
+
# Production build with Subresource Integrity digests
|
|
187
|
+
bun_bun_bundle build --prod --sri=sha384
|
|
188
|
+
|
|
186
189
|
# Development with verbose WebSocket logging
|
|
187
190
|
bun_bun_bundle dev --debug
|
|
188
191
|
```
|
|
@@ -196,8 +199,16 @@ bun_bun_bundle dev --debug
|
|
|
196
199
|
to `inline` in `dev` and `linked` for builds, so production stack traces
|
|
197
200
|
and browser devtools stay debuggable. Pass `--sourcemap=none` when you
|
|
198
201
|
explicitly do not want maps shipped.
|
|
202
|
+
- `--sri[=ALGOS]`: compute [Subresource Integrity][sri] digests for each
|
|
203
|
+
asset. Pass a comma-separated list of `sha256`, `sha384`, or `sha512`
|
|
204
|
+
(bare `--sri` defaults to `sha384`). When digests are present, the
|
|
205
|
+
`bun_js_tag` and `bun_css_tag` helpers automatically render
|
|
206
|
+
`integrity="..." crossorigin="anonymous"` so browsers verify the response
|
|
207
|
+
before executing it
|
|
199
208
|
- `--debug`: verbose WebSocket logging
|
|
200
209
|
|
|
210
|
+
[sri]: https://developer.mozilla.org/docs/Web/Security/Subresource_Integrity
|
|
211
|
+
|
|
201
212
|
> [!NOTE]
|
|
202
213
|
> When running from a Procfile (e.g. with Overmind or Foreman), use
|
|
203
214
|
> `bundle exec bun_bun_bundle` to ensure the correct gem version is loaded.
|
data/exe/bun_bun_bundle
CHANGED
|
@@ -27,6 +27,9 @@ else
|
|
|
27
27
|
--minify Minify JS and CSS output
|
|
28
28
|
--sourcemap[=KIND] Sourcemap kind: inline, linked, external, none
|
|
29
29
|
(dev defaults to inline, builds default to linked)
|
|
30
|
+
--sri[=ALGOS] Compute Subresource Integrity digests for each
|
|
31
|
+
asset (comma-separated: sha256, sha384, sha512;
|
|
32
|
+
defaults to sha384)
|
|
30
33
|
--debug Enable verbose WebSocket logging
|
|
31
34
|
|
|
32
35
|
Examples:
|
|
@@ -34,6 +37,7 @@ else
|
|
|
34
37
|
bun_bun_bundle build # plain build
|
|
35
38
|
bun_bun_bundle build --prod # fingerprint + minify
|
|
36
39
|
bun_bun_bundle build --fingerprint # fingerprint only
|
|
40
|
+
bun_bun_bundle build --prod --sri=sha384 # add integrity digests
|
|
37
41
|
bun_bun_bundle build --prod --sourcemap=none # strip sourcemaps
|
|
38
42
|
|
|
39
43
|
Configuration:
|
data/lib/bun/bun_bundle.js
CHANGED
|
@@ -24,16 +24,17 @@ export default {
|
|
|
24
24
|
fingerprint: false,
|
|
25
25
|
minify: false,
|
|
26
26
|
sourcemap: null,
|
|
27
|
+
sri: [],
|
|
27
28
|
wsClients: new Set(),
|
|
28
29
|
watchTimers: new Map(),
|
|
30
|
+
watchers: [],
|
|
29
31
|
plugins: [],
|
|
30
32
|
|
|
33
|
+
SRI_ALGORITHMS: ['sha256', 'sha384', 'sha512'],
|
|
34
|
+
|
|
31
35
|
flags(input) {
|
|
32
|
-
const {debug, dev, prod, fingerprint, minify, sourcemap} =
|
|
33
|
-
input
|
|
34
|
-
)
|
|
35
|
-
? this.parseArgv(input)
|
|
36
|
-
: input
|
|
36
|
+
const {debug, dev, prod, fingerprint, minify, sourcemap, sri} =
|
|
37
|
+
Array.isArray(input) ? this.parseArgv(input) : input
|
|
37
38
|
if (debug != null) this.debug = debug
|
|
38
39
|
if (dev != null) this.dev = dev
|
|
39
40
|
if (prod != null) this.prod = prod
|
|
@@ -42,31 +43,74 @@ export default {
|
|
|
42
43
|
if (minify != null) this.minify = minify
|
|
43
44
|
else if (prod === true) this.minify = true
|
|
44
45
|
if (sourcemap != null) this.sourcemap = sourcemap
|
|
46
|
+
if (sri != null) this.sri = sri
|
|
45
47
|
},
|
|
46
48
|
|
|
47
49
|
SOURCEMAP_KINDS: ['inline', 'linked', 'external', 'none'],
|
|
50
|
+
BOOLEAN_FLAGS: ['debug', 'dev', 'prod', 'fingerprint', 'minify'],
|
|
48
51
|
|
|
49
52
|
parseArgv(argv) {
|
|
53
|
+
return {
|
|
54
|
+
...this.parseBooleanFlags(argv),
|
|
55
|
+
...this.parseSourcemapFlag(argv),
|
|
56
|
+
...this.parseSriFlag(argv)
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
parseBooleanFlags(argv) {
|
|
50
61
|
const opts = {}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (argv.includes('--prod')) opts.prod = true
|
|
54
|
-
if (argv.includes('--fingerprint')) opts.fingerprint = true
|
|
55
|
-
if (argv.includes('--minify')) opts.minify = true
|
|
56
|
-
const sm = argv.find(
|
|
57
|
-
a => a === '--sourcemap' || a.startsWith('--sourcemap=')
|
|
58
|
-
)
|
|
59
|
-
if (sm) {
|
|
60
|
-
const value = sm.includes('=') ? sm.split('=')[1] : 'linked'
|
|
61
|
-
if (this.SOURCEMAP_KINDS.includes(value)) opts.sourcemap = value
|
|
62
|
-
else
|
|
63
|
-
console.warn(
|
|
64
|
-
` ▸ Ignoring --sourcemap=${value} (valid: ${this.SOURCEMAP_KINDS.join(', ')})`
|
|
65
|
-
)
|
|
62
|
+
for (const name of this.BOOLEAN_FLAGS) {
|
|
63
|
+
if (argv.includes(`--${name}`)) opts[name] = true
|
|
66
64
|
}
|
|
67
65
|
return opts
|
|
68
66
|
},
|
|
69
67
|
|
|
68
|
+
findValueFlag(argv, name, defaultValue) {
|
|
69
|
+
const flag = argv.find(a => a === `--${name}` || a.startsWith(`--${name}=`))
|
|
70
|
+
if (!flag) return null
|
|
71
|
+
return flag.includes('=') ? flag.split('=')[1] : defaultValue
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
parseSourcemapFlag(argv) {
|
|
75
|
+
const value = this.findValueFlag(argv, 'sourcemap', 'linked')
|
|
76
|
+
if (value === null) return {}
|
|
77
|
+
if (this.SOURCEMAP_KINDS.includes(value)) return {sourcemap: value}
|
|
78
|
+
console.warn(
|
|
79
|
+
` ▸ Ignoring --sourcemap=${value} (valid: ${this.SOURCEMAP_KINDS.join(', ')})`
|
|
80
|
+
)
|
|
81
|
+
return {}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
parseSriFlag(argv) {
|
|
85
|
+
const value = this.findValueFlag(argv, 'sri', 'sha384')
|
|
86
|
+
if (value === null) return {}
|
|
87
|
+
const algos = value.split(',').map(a => a.trim()).filter(Boolean)
|
|
88
|
+
const valid = algos.filter(a => this.SRI_ALGORITHMS.includes(a))
|
|
89
|
+
const invalid = algos.filter(a => !this.SRI_ALGORITHMS.includes(a))
|
|
90
|
+
if (invalid.length) {
|
|
91
|
+
console.warn(
|
|
92
|
+
` ▸ Ignoring --sri=${invalid.join(',')} (valid: ${this.SRI_ALGORITHMS.join(', ')})`
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
return valid.length ? {sri: valid} : {}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
computeSri(content) {
|
|
99
|
+
if (!this.sri.length) return null
|
|
100
|
+
return this.sri.map(algo => {
|
|
101
|
+
const hasher = new Bun.CryptoHasher(algo)
|
|
102
|
+
hasher.update(content)
|
|
103
|
+
return `${algo}-${hasher.digest('base64')}`
|
|
104
|
+
})
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
manifestEntry(url, content) {
|
|
108
|
+
const entry = {url}
|
|
109
|
+
const sri = this.computeSri(content)
|
|
110
|
+
if (sri) entry.sri = sri
|
|
111
|
+
return entry
|
|
112
|
+
},
|
|
113
|
+
|
|
70
114
|
deepMerge(target, source) {
|
|
71
115
|
const result = {...target}
|
|
72
116
|
for (const k of Object.keys(source))
|
|
@@ -186,7 +230,10 @@ export default {
|
|
|
186
230
|
}
|
|
187
231
|
|
|
188
232
|
await Bun.write(join(outDir, fileName), content)
|
|
189
|
-
this.manifest[`${type}/${entryName}${ext}`] =
|
|
233
|
+
this.manifest[`${type}/${entryName}${ext}`] = this.manifestEntry(
|
|
234
|
+
`${type}/${fileName}`,
|
|
235
|
+
content
|
|
236
|
+
)
|
|
190
237
|
}
|
|
191
238
|
},
|
|
192
239
|
|
|
@@ -215,20 +262,20 @@ export default {
|
|
|
215
262
|
for await (const file of glob.scan({cwd: fullDir, onlyFiles: true})) {
|
|
216
263
|
const srcPath = join(fullDir, file)
|
|
217
264
|
const content = await Bun.file(srcPath).arrayBuffer()
|
|
265
|
+
const bytes = new Uint8Array(content)
|
|
218
266
|
|
|
219
267
|
const ext = extname(file)
|
|
220
268
|
const name = file.slice(0, -ext.length) || file
|
|
221
|
-
const fileName = this.fingerprintName(
|
|
222
|
-
name,
|
|
223
|
-
ext,
|
|
224
|
-
new Uint8Array(content)
|
|
225
|
-
)
|
|
269
|
+
const fileName = this.fingerprintName(name, ext, bytes)
|
|
226
270
|
const destPath = join(destDir, fileName)
|
|
227
271
|
|
|
228
272
|
mkdirSync(dirname(destPath), {recursive: true})
|
|
229
273
|
await Bun.write(destPath, content)
|
|
230
274
|
|
|
231
|
-
this.manifest[`${assetType}/${file}`] =
|
|
275
|
+
this.manifest[`${assetType}/${file}`] = this.manifestEntry(
|
|
276
|
+
`${assetType}/${fileName}`,
|
|
277
|
+
bytes
|
|
278
|
+
)
|
|
232
279
|
}
|
|
233
280
|
}
|
|
234
281
|
},
|
|
@@ -260,7 +307,10 @@ export default {
|
|
|
260
307
|
|
|
261
308
|
prettyManifest() {
|
|
262
309
|
const lines = Object.entries(this.manifest)
|
|
263
|
-
.map(([key, value]) =>
|
|
310
|
+
.map(([key, value]) => {
|
|
311
|
+
const url = value && typeof value === 'object' ? value.url : value
|
|
312
|
+
return ` ${key} → ${url}`
|
|
313
|
+
})
|
|
264
314
|
.join('\n')
|
|
265
315
|
return `\n${lines}\n\n`
|
|
266
316
|
},
|
|
@@ -338,12 +388,27 @@ export default {
|
|
|
338
388
|
console.warn(` ▸ Watch directory ${dir} does not exist, skipping...`)
|
|
339
389
|
continue
|
|
340
390
|
}
|
|
341
|
-
watch(fullDir, {recursive: true}, handler)
|
|
391
|
+
this.watchers.push(watch(fullDir, {recursive: true}, handler))
|
|
342
392
|
}
|
|
343
393
|
|
|
344
394
|
console.log('Beginning to watch your project')
|
|
345
395
|
},
|
|
346
396
|
|
|
397
|
+
shutdown() {
|
|
398
|
+
for (const w of this.watchers) {
|
|
399
|
+
try {
|
|
400
|
+
w.close()
|
|
401
|
+
} catch {}
|
|
402
|
+
}
|
|
403
|
+
this.watchers = []
|
|
404
|
+
for (const client of this.wsClients) {
|
|
405
|
+
try {
|
|
406
|
+
client.close()
|
|
407
|
+
} catch {}
|
|
408
|
+
}
|
|
409
|
+
this.wsClients.clear()
|
|
410
|
+
},
|
|
411
|
+
|
|
347
412
|
async serve() {
|
|
348
413
|
await this.build()
|
|
349
414
|
await this.watch()
|
|
@@ -353,7 +418,7 @@ export default {
|
|
|
353
418
|
const debug = this.debug
|
|
354
419
|
const wsClients = this.wsClients
|
|
355
420
|
|
|
356
|
-
Bun.serve({
|
|
421
|
+
const server = Bun.serve({
|
|
357
422
|
hostname,
|
|
358
423
|
port,
|
|
359
424
|
fetch(req, server) {
|
|
@@ -376,6 +441,15 @@ export default {
|
|
|
376
441
|
|
|
377
442
|
const protocol = secure ? 'wss' : 'ws'
|
|
378
443
|
console.log(`\n\n 🔌 Live reload at ${protocol}://${host}:${port}\n\n`)
|
|
444
|
+
|
|
445
|
+
process.on('SIGINT', () => {
|
|
446
|
+
console.log('\n ▸ Shutting down...')
|
|
447
|
+
this.shutdown()
|
|
448
|
+
try {
|
|
449
|
+
server.stop(true)
|
|
450
|
+
} catch {}
|
|
451
|
+
process.exit(0)
|
|
452
|
+
})
|
|
379
453
|
},
|
|
380
454
|
|
|
381
455
|
async bake() {
|
|
@@ -15,6 +15,8 @@ module BunBunBundle
|
|
|
15
15
|
module Helpers
|
|
16
16
|
include SafeHtml
|
|
17
17
|
|
|
18
|
+
FINGERPRINTED_CSS_REGEX = /-[0-9a-f]{8}\.css$/
|
|
19
|
+
|
|
18
20
|
# Returns the public path to an asset from the manifest.
|
|
19
21
|
#
|
|
20
22
|
# Prepends the configured public_path and asset_host:
|
|
@@ -23,8 +25,7 @@ module BunBunBundle
|
|
|
23
25
|
# bun_asset("images/logo.png") # => "/assets/images/logo-abc123.png" (production)
|
|
24
26
|
#
|
|
25
27
|
def bun_asset(path)
|
|
26
|
-
|
|
27
|
-
"#{BunBunBundle.asset_host}#{BunBunBundle.config.public_path}/#{fingerprinted}"
|
|
28
|
+
bun_asset_url(bun_entry(path))
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
# Generates a <script> tag for a JS entry point.
|
|
@@ -33,8 +34,11 @@ module BunBunBundle
|
|
|
33
34
|
# # => '<script src="/assets/js/app.js" type="text/javascript"></script>'
|
|
34
35
|
#
|
|
35
36
|
def bun_js_tag(source, **options)
|
|
36
|
-
|
|
37
|
-
attrs = { type: 'text/javascript' }
|
|
37
|
+
entry = bun_entry(source)
|
|
38
|
+
attrs = { type: 'text/javascript' }
|
|
39
|
+
.merge(bun_sri_attrs(entry))
|
|
40
|
+
.merge(options)
|
|
41
|
+
.merge(src: bun_asset_url(entry))
|
|
38
42
|
bun_safe(%(<script #{bun_html_attrs(attrs)}></script>))
|
|
39
43
|
end
|
|
40
44
|
|
|
@@ -44,14 +48,19 @@ module BunBunBundle
|
|
|
44
48
|
# # => '<link href="/assets/css/app.css" type="text/css" rel="stylesheet">'
|
|
45
49
|
#
|
|
46
50
|
def bun_css_tag(source, **options)
|
|
51
|
+
entry = bun_entry(source)
|
|
47
52
|
attrs = { type: 'text/css', rel: 'stylesheet' }
|
|
53
|
+
.merge(bun_sri_attrs(entry))
|
|
48
54
|
.merge(options)
|
|
49
|
-
.merge(href: bun_href_with_timestamp(
|
|
55
|
+
.merge(href: bun_href_with_timestamp(bun_asset_url(entry)))
|
|
50
56
|
bun_safe(%(<link #{bun_html_attrs(attrs)}>))
|
|
51
57
|
end
|
|
52
58
|
|
|
53
59
|
# Generates an <img> tag for an image asset.
|
|
54
60
|
#
|
|
61
|
+
# Subresource Integrity is intentionally not rendered on images: browsers
|
|
62
|
+
# do not enforce SRI for <img> elements.
|
|
63
|
+
#
|
|
55
64
|
# bun_img_tag("images/logo.png", alt: "Logo")
|
|
56
65
|
# # => '<img src="/assets/images/logo.png" alt="Logo">'
|
|
57
66
|
#
|
|
@@ -64,6 +73,20 @@ module BunBunBundle
|
|
|
64
73
|
|
|
65
74
|
private
|
|
66
75
|
|
|
76
|
+
def bun_entry(source)
|
|
77
|
+
BunBunBundle.manifest[source]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def bun_asset_url(entry)
|
|
81
|
+
"#{BunBunBundle.asset_host}#{BunBunBundle.config.public_path}/#{entry.url}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def bun_sri_attrs(entry)
|
|
85
|
+
return {} if entry.sri.empty?
|
|
86
|
+
|
|
87
|
+
{ integrity: entry.sri.join(' '), crossorigin: 'anonymous' }
|
|
88
|
+
end
|
|
89
|
+
|
|
67
90
|
def bun_html_attrs(hash)
|
|
68
91
|
bun_flatten_attrs(hash).compact.map do |k, v|
|
|
69
92
|
k = k.to_s.tr('_', '-')
|
|
@@ -89,7 +112,7 @@ module BunBunBundle
|
|
|
89
112
|
def bun_href_with_timestamp(href)
|
|
90
113
|
config = BunBunBundle.config
|
|
91
114
|
return href unless href.start_with?(config.public_path)
|
|
92
|
-
return href if href.match?(
|
|
115
|
+
return href if href.match?(FINGERPRINTED_CSS_REGEX)
|
|
93
116
|
|
|
94
117
|
file_path = href.sub(config.public_path, config.out_dir)
|
|
95
118
|
mtime = File.exist?(file_path) ? File.mtime(file_path).to_i : Time.now.to_i
|
|
@@ -5,11 +5,30 @@ require 'json'
|
|
|
5
5
|
module BunBunBundle
|
|
6
6
|
class Manifest
|
|
7
7
|
class MissingAssetError < StandardError; end
|
|
8
|
+
class MigrationError < StandardError; end
|
|
9
|
+
class InvalidEntryError < StandardError; end
|
|
10
|
+
|
|
11
|
+
Entry = Data.define(:url, :sri) do
|
|
12
|
+
def self.from(value)
|
|
13
|
+
value.is_a?(Hash) || raise(
|
|
14
|
+
MigrationError,
|
|
15
|
+
'Manifest predates bun_bun_bundle 0.13. Run: bun_bun_bundle build',
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if (url = value['url']).nil? || url.empty?
|
|
19
|
+
raise InvalidEntryError, "Manifest entry is missing 'url'"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
new(url: url, sri: Array(value['sri']).freeze)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
8
25
|
|
|
9
26
|
attr_reader :entries
|
|
10
27
|
|
|
11
28
|
def initialize(entries = {})
|
|
12
|
-
@entries = entries.
|
|
29
|
+
@entries = entries.transform_values do |value|
|
|
30
|
+
value.is_a?(Entry) ? value : Entry.from(value)
|
|
31
|
+
end.freeze
|
|
13
32
|
end
|
|
14
33
|
|
|
15
34
|
# Loads the manifest from a JSON file.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bun_bun_bundle
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Wout Fierens
|
|
@@ -73,7 +73,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
74
|
version: '0'
|
|
75
75
|
requirements: []
|
|
76
|
-
rubygems_version: 4.0.
|
|
76
|
+
rubygems_version: 4.0.11
|
|
77
77
|
specification_version: 4
|
|
78
78
|
summary: A self-contained asset bundler powered by Bun
|
|
79
79
|
test_files: []
|