bun_bun_bundle 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +296 -0
- data/exe/bun_bun_bundle +38 -0
- data/lib/bun/bake.js +8 -0
- data/lib/bun/bun_bundle.js +276 -0
- data/lib/bun/plugins/cssAliases.js +10 -0
- data/lib/bun/plugins/cssGlobs.js +47 -0
- data/lib/bun/plugins/index.js +85 -0
- data/lib/bun/plugins/jsGlobs.js +47 -0
- data/lib/bun_bun_bundle/config.rb +58 -0
- data/lib/bun_bun_bundle/dev_cache_middleware.rb +50 -0
- data/lib/bun_bun_bundle/hanami.rb +46 -0
- data/lib/bun_bun_bundle/helpers.rb +74 -0
- data/lib/bun_bun_bundle/manifest.rb +81 -0
- data/lib/bun_bun_bundle/railtie.rb +30 -0
- data/lib/bun_bun_bundle/reload_tag.rb +61 -0
- data/lib/bun_bun_bundle/version.rb +5 -0
- data/lib/bun_bun_bundle.rb +53 -0
- metadata +77 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {join} from 'path'
|
|
2
|
+
import cssAliases from './cssAliases.js'
|
|
3
|
+
import cssGlobs from './cssGlobs.js'
|
|
4
|
+
import jsGlobs from './jsGlobs.js'
|
|
5
|
+
|
|
6
|
+
const builtins = {cssAliases, cssGlobs, jsGlobs}
|
|
7
|
+
const TYPE_REGEXES = {
|
|
8
|
+
css: /\.css$/,
|
|
9
|
+
js: /\.(js|ts|jsx|tsx)$/
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Combines transform functions into a single Bun plugin for a given file type.
|
|
13
|
+
function transformPipeline(type, transforms) {
|
|
14
|
+
const filter = TYPE_REGEXES[type]
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
name: `${type}-transforms`,
|
|
18
|
+
setup(build) {
|
|
19
|
+
build.onLoad({filter}, async args => {
|
|
20
|
+
let content = await Bun.file(args.path).text()
|
|
21
|
+
for (const transform of transforms)
|
|
22
|
+
content = await transform(content, args)
|
|
23
|
+
return {contents: content, loader: type}
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Resolves a plugin name or path into a transform function.
|
|
30
|
+
async function loadFactory(name, root) {
|
|
31
|
+
if (builtins[name]) return builtins[name]
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const mod = await import(join(root, name))
|
|
35
|
+
console.log(` ▸ Loaded custom plugin: ${name}`)
|
|
36
|
+
return mod.default || mod
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error(` ✖ Failed to load plugin "${name}": ${err.message}`)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Resolves plugin names into transforms for a single type.
|
|
43
|
+
async function resolveType(names, context) {
|
|
44
|
+
const transforms = []
|
|
45
|
+
const plugins = []
|
|
46
|
+
|
|
47
|
+
for (const name of names) {
|
|
48
|
+
const factory = await loadFactory(name, context.root)
|
|
49
|
+
if (typeof factory !== 'function') {
|
|
50
|
+
if (factory != null)
|
|
51
|
+
console.error(` ✖ Plugin "${name}" does not export a function`)
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const result = factory(context)
|
|
56
|
+
if (typeof result === 'function') transforms.push(result)
|
|
57
|
+
else if (result?.setup) plugins.push(result)
|
|
58
|
+
else console.error(` ✖ Plugin "${name}" returned an invalid value`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {transforms, plugins}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Resolves plugin config into Bun plugin instances.
|
|
65
|
+
export async function resolvePlugins(pluginConfig, context) {
|
|
66
|
+
const bunPlugins = []
|
|
67
|
+
|
|
68
|
+
if (!pluginConfig || typeof pluginConfig !== 'object') return bunPlugins
|
|
69
|
+
|
|
70
|
+
for (const [type, names] of Object.entries(pluginConfig)) {
|
|
71
|
+
if (!Array.isArray(names)) continue
|
|
72
|
+
|
|
73
|
+
if (!TYPE_REGEXES[type]) {
|
|
74
|
+
console.error(` ✖ Unknown plugin type "${type}"`)
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const {transforms, plugins} = await resolveType(names, context)
|
|
79
|
+
bunPlugins.push(...plugins)
|
|
80
|
+
if (transforms.length)
|
|
81
|
+
bunPlugins.unshift(transformPipeline(type, transforms))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return bunPlugins
|
|
85
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {dirname, extname} from 'path'
|
|
2
|
+
import {Glob} from 'bun'
|
|
3
|
+
|
|
4
|
+
const REGEX = /import\s+(\w+)\s+from\s+['"]glob:([^'"]+)['"]/g
|
|
5
|
+
|
|
6
|
+
// Compiles an object with a file path => default export mapping from a glob
|
|
7
|
+
// pattern in JS import statements.
|
|
8
|
+
// e.g. import components from 'glob:./components/**/*.js'
|
|
9
|
+
//
|
|
10
|
+
// ... will generate ...
|
|
11
|
+
//
|
|
12
|
+
// import _glob_components_theme from './components/theme.js'
|
|
13
|
+
// import _glob_components_shared_tooltip from './components/shared/tooltip.js'
|
|
14
|
+
// const components = {
|
|
15
|
+
// 'components/theme': _glob_components_theme,
|
|
16
|
+
// 'components/shared/tooltip': _glob_components_shared_tooltip
|
|
17
|
+
// }
|
|
18
|
+
export default function jsGlobs() {
|
|
19
|
+
return (content, args) => {
|
|
20
|
+
return content.replace(REGEX, (_, binding, pattern) => {
|
|
21
|
+
const dir = dirname(args.path)
|
|
22
|
+
const cleanPattern = pattern.replace(/^\.\//, '')
|
|
23
|
+
const glob = new Glob(cleanPattern)
|
|
24
|
+
const files = Array.from(glob.scanSync({cwd: dir})).sort()
|
|
25
|
+
|
|
26
|
+
if (!files.length) return `const ${binding} = {}`
|
|
27
|
+
|
|
28
|
+
const imports = []
|
|
29
|
+
const entries = []
|
|
30
|
+
|
|
31
|
+
for (const file of files) {
|
|
32
|
+
const ext = extname(file)
|
|
33
|
+
const key = file.slice(0, -ext.length)
|
|
34
|
+
const safe = `_glob_${key.replace(/[^a-zA-Z0-9]/g, '_')}`
|
|
35
|
+
imports.push(`import ${safe} from './${file}'`)
|
|
36
|
+
entries.push(` '${key}': ${safe}`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return [
|
|
40
|
+
...imports,
|
|
41
|
+
`const ${binding} = {`,
|
|
42
|
+
entries.join(',\n'),
|
|
43
|
+
'}'
|
|
44
|
+
].join('\n')
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module BunBunBundle
|
|
6
|
+
class Config
|
|
7
|
+
CONFIG_PATH = 'config/bun.json'
|
|
8
|
+
|
|
9
|
+
attr_reader :manifest_path, :out_dir, :public_path, :static_dirs,
|
|
10
|
+
:entry_points, :dev_server
|
|
11
|
+
|
|
12
|
+
def initialize(data = {})
|
|
13
|
+
@manifest_path = data.fetch('manifestPath', 'public/bun-manifest.json')
|
|
14
|
+
@out_dir = data.fetch('outDir', 'public/assets')
|
|
15
|
+
@public_path = data.fetch('publicPath', '/assets')
|
|
16
|
+
@static_dirs = data.fetch('staticDirs', %w[app/assets/images app/assets/fonts])
|
|
17
|
+
@entry_points = EntryPoints.new(data.fetch('entryPoints', {}))
|
|
18
|
+
@dev_server = DevServer.new(data.fetch('devServer', {}))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.load(root: Dir.pwd)
|
|
22
|
+
path = File.join(root, CONFIG_PATH)
|
|
23
|
+
data = File.exist?(path) ? JSON.parse(File.read(path)) : {}
|
|
24
|
+
new(data)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class EntryPoints
|
|
28
|
+
attr_reader :js, :css
|
|
29
|
+
|
|
30
|
+
def initialize(data = {})
|
|
31
|
+
@js = data.fetch('js', %w[app/assets/js/app.js])
|
|
32
|
+
@css = data.fetch('css', %w[app/assets/css/app.css])
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class DevServer
|
|
37
|
+
attr_reader :host, :port
|
|
38
|
+
|
|
39
|
+
def initialize(data = {})
|
|
40
|
+
@host = data.fetch('host', '127.0.0.1')
|
|
41
|
+
@port = data.fetch('port', 3002)
|
|
42
|
+
@secure = data.fetch('secure', false)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def secure?
|
|
46
|
+
@secure
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def ws_protocol
|
|
50
|
+
secure? ? 'wss' : 'ws'
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def ws_url
|
|
54
|
+
"#{ws_protocol}://#{host}:#{port}"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BunBunBundle
|
|
4
|
+
# Rack middleware that sets no-cache headers on asset requests in development.
|
|
5
|
+
#
|
|
6
|
+
# This ensures the browser always fetches fresh assets during development,
|
|
7
|
+
# avoiding stale-cache issues when files are rebuilt.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
#
|
|
11
|
+
# # In Rails:
|
|
12
|
+
# # Automatically added by the Railtie in development.
|
|
13
|
+
#
|
|
14
|
+
# # In Hanami:
|
|
15
|
+
# # Automatically added by the Hanami integration in development.
|
|
16
|
+
#
|
|
17
|
+
# # Manual Rack usage:
|
|
18
|
+
# use BunBunBundle::DevCacheMiddleware
|
|
19
|
+
#
|
|
20
|
+
class DevCacheMiddleware
|
|
21
|
+
ASSET_EXTENSIONS = %w[
|
|
22
|
+
.js .css .map .json
|
|
23
|
+
.png .jpg .jpeg .gif .svg .webp
|
|
24
|
+
.woff .woff2 .ttf .eot
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
def initialize(app)
|
|
28
|
+
@app = app
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def call(env)
|
|
32
|
+
status, headers, body = @app.call(env)
|
|
33
|
+
|
|
34
|
+
if asset_request?(env['PATH_INFO'])
|
|
35
|
+
headers['Cache-Control'] = 'no-store, no-cache, must-revalidate'
|
|
36
|
+
headers['Expires'] = '0'
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
[status, headers, body]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def asset_request?(path)
|
|
45
|
+
return false unless path
|
|
46
|
+
|
|
47
|
+
ASSET_EXTENSIONS.any? { |ext| path.end_with?(ext) }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BunBunBundle
|
|
4
|
+
# Hanami integration for BunBunBundle.
|
|
5
|
+
#
|
|
6
|
+
# Add to your Hanami app's config/app.rb:
|
|
7
|
+
#
|
|
8
|
+
# require "bun_bun_bundle/hanami"
|
|
9
|
+
#
|
|
10
|
+
# module MyApp
|
|
11
|
+
# class App < Hanami::App
|
|
12
|
+
# config.middleware.use BunBunBundle::DevCacheMiddleware if Hanami.env?(:development)
|
|
13
|
+
# end
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# Then include helpers in your views:
|
|
17
|
+
#
|
|
18
|
+
# # app/views/helpers.rb
|
|
19
|
+
# module MyApp
|
|
20
|
+
# module Views
|
|
21
|
+
# module Helpers
|
|
22
|
+
# include BunBunBundle::Helpers
|
|
23
|
+
# include BunBunBundle::ReloadTag
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
module HanamiIntegration
|
|
29
|
+
def self.setup(root: Dir.pwd)
|
|
30
|
+
BunBunBundle.config = Config.load(root: root)
|
|
31
|
+
|
|
32
|
+
BunBunBundle.manifest = if BunBunBundle.development?
|
|
33
|
+
Manifest.load(root: root)
|
|
34
|
+
else
|
|
35
|
+
Manifest.load(root: root, retries: 1, delay: 0)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Auto-setup when loaded in a Hanami app.
|
|
42
|
+
if defined?(Hanami)
|
|
43
|
+
Hanami::App.after :configure do
|
|
44
|
+
BunBunBundle::HanamiIntegration.setup(root: root.to_s)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BunBunBundle
|
|
4
|
+
# Asset helpers for use in views/templates.
|
|
5
|
+
#
|
|
6
|
+
# Include this module in your view helpers to get access to `bun_asset` and
|
|
7
|
+
# tag helpers. Works with ERB, Haml, Slim, or any other templating engine.
|
|
8
|
+
#
|
|
9
|
+
# Example (ERB):
|
|
10
|
+
#
|
|
11
|
+
# <img src="<%= bun_asset("images/logo.png") %>">
|
|
12
|
+
# <%= bun_js_tag("js/app.js") %>
|
|
13
|
+
# <%= bun_css_tag("css/app.css") %>
|
|
14
|
+
#
|
|
15
|
+
module Helpers
|
|
16
|
+
# Returns the public path to an asset from the manifest.
|
|
17
|
+
#
|
|
18
|
+
# Prepends the configured public_path and asset_host:
|
|
19
|
+
#
|
|
20
|
+
# bun_asset("js/app.js") # => "/assets/js/app.js"
|
|
21
|
+
# bun_asset("images/logo.png") # => "/assets/images/logo-abc123.png" (production)
|
|
22
|
+
#
|
|
23
|
+
def bun_asset(path)
|
|
24
|
+
fingerprinted = BunBunBundle.manifest[path]
|
|
25
|
+
"#{BunBunBundle.asset_host}#{BunBunBundle.config.public_path}/#{fingerprinted}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Generates a <script> tag for a JS entry point.
|
|
29
|
+
#
|
|
30
|
+
# bun_js_tag("js/app.js")
|
|
31
|
+
# # => '<script src="/assets/js/app.js" type="text/javascript"></script>'
|
|
32
|
+
#
|
|
33
|
+
def bun_js_tag(source, **options)
|
|
34
|
+
src = bun_asset(source)
|
|
35
|
+
attrs = { type: 'text/javascript' }.merge(options).merge(src: src)
|
|
36
|
+
%(<script #{_bun_html_attrs(attrs)}></script>)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Generates a <link> tag for a CSS entry point.
|
|
40
|
+
#
|
|
41
|
+
# bun_css_tag("css/app.css")
|
|
42
|
+
# # => '<link href="/assets/css/app.css" type="text/css" rel="stylesheet">'
|
|
43
|
+
#
|
|
44
|
+
def bun_css_tag(source, **options)
|
|
45
|
+
href = bun_asset(source)
|
|
46
|
+
attrs = { type: 'text/css', rel: 'stylesheet' }.merge(options).merge(href: href)
|
|
47
|
+
%(<link #{_bun_html_attrs(attrs)}>)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Generates an <img> tag for an image asset.
|
|
51
|
+
#
|
|
52
|
+
# bun_img_tag("images/logo.png", alt: "Logo")
|
|
53
|
+
# # => '<img src="/assets/images/logo.png" alt="Logo">'
|
|
54
|
+
#
|
|
55
|
+
def bun_img_tag(source, **options)
|
|
56
|
+
src = bun_asset(source)
|
|
57
|
+
alt = options.delete(:alt) || File.basename(source, '.*').tr('-_', ' ').capitalize
|
|
58
|
+
attrs = { alt: alt }.merge(options).merge(src: src)
|
|
59
|
+
%(<img #{_bun_html_attrs(attrs)}>)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def _bun_html_attrs(hash)
|
|
65
|
+
hash.compact.map do |k, v|
|
|
66
|
+
v == true ? k.to_s : %(#{k}="#{_bun_escape_attr(v)}")
|
|
67
|
+
end.join(' ')
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def _bun_escape_attr(value)
|
|
71
|
+
value.to_s.gsub('&', '&').gsub('"', '"').gsub('<', '<').gsub('>', '>')
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module BunBunBundle
|
|
6
|
+
class Manifest
|
|
7
|
+
class MissingAssetError < StandardError; end
|
|
8
|
+
|
|
9
|
+
attr_reader :entries
|
|
10
|
+
|
|
11
|
+
def initialize(entries = {})
|
|
12
|
+
@entries = entries.freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Loads the manifest from a JSON file.
|
|
16
|
+
#
|
|
17
|
+
# Retries a configurable number of times to allow for the manifest to be
|
|
18
|
+
# built during boot (e.g. in development).
|
|
19
|
+
def self.load(path: nil, root: Dir.pwd, retries: 10, delay: 0.25)
|
|
20
|
+
path ||= BunBunBundle.config.manifest_path
|
|
21
|
+
full_path = File.expand_path(path, root)
|
|
22
|
+
|
|
23
|
+
retries.times do
|
|
24
|
+
if File.exist?(full_path)
|
|
25
|
+
data = JSON.parse(File.read(full_path))
|
|
26
|
+
return new(data)
|
|
27
|
+
end
|
|
28
|
+
sleep(delay)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
raise "Manifest not found at #{full_path}. Run: bun_bun_bundle build"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def [](key)
|
|
35
|
+
entries.fetch(key) do
|
|
36
|
+
suggestion = find_similar(key)
|
|
37
|
+
message = "Asset not found: #{key}"
|
|
38
|
+
message += ". Did you mean: #{suggestion}?" if suggestion
|
|
39
|
+
raise MissingAssetError, message
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def key?(key)
|
|
44
|
+
entries.key?(key)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def css_entry_points
|
|
48
|
+
entries.keys.select { |k| k.end_with?('.css') }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def find_similar(key, tolerance: 4)
|
|
54
|
+
entries.keys
|
|
55
|
+
.map { |k| [k, levenshtein(key, k)] }
|
|
56
|
+
.select { |_, d| d <= tolerance }
|
|
57
|
+
.min_by { |_, d| d }
|
|
58
|
+
&.first
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def levenshtein(str_a, str_b) # rubocop:disable Metrics/AbcSize
|
|
62
|
+
return str_b.length if str_a.empty?
|
|
63
|
+
return str_a.length if str_b.empty?
|
|
64
|
+
|
|
65
|
+
distances = Array.new(str_a.length + 1) { |i| i }
|
|
66
|
+
|
|
67
|
+
(1..str_b.length).each do |j|
|
|
68
|
+
prev = distances[0]
|
|
69
|
+
distances[0] = j
|
|
70
|
+
(1..str_a.length).each do |i|
|
|
71
|
+
cost = str_a[i - 1] == str_b[j - 1] ? 0 : 1
|
|
72
|
+
temp = distances[i]
|
|
73
|
+
distances[i] = [distances[i] + 1, distances[i - 1] + 1, prev + cost].min
|
|
74
|
+
prev = temp
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
distances[str_a.length]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/railtie'
|
|
4
|
+
|
|
5
|
+
module BunBunBundle
|
|
6
|
+
class Railtie < Rails::Railtie
|
|
7
|
+
initializer 'bun_bun_bundle.helpers' do
|
|
8
|
+
ActiveSupport.on_load(:action_view) do
|
|
9
|
+
include BunBunBundle::Helpers
|
|
10
|
+
include BunBunBundle::ReloadTag
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
initializer 'bun_bun_bundle.manifest' do |app|
|
|
15
|
+
config.after_initialize do
|
|
16
|
+
BunBunBundle.config = Config.load(root: app.root.to_s)
|
|
17
|
+
|
|
18
|
+
BunBunBundle.manifest = if BunBunBundle.development?
|
|
19
|
+
Manifest.load(root: app.root.to_s)
|
|
20
|
+
else
|
|
21
|
+
Manifest.load(root: app.root.to_s, retries: 1, delay: 0)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
initializer 'bun_bun_bundle.dev_cache' do |app|
|
|
27
|
+
app.middleware.insert_before(0, BunBunBundle::DevCacheMiddleware) if BunBunBundle.development?
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BunBunBundle
|
|
4
|
+
# Renders a live reload script tag for development.
|
|
5
|
+
#
|
|
6
|
+
# Connects to Bun's WebSocket dev server for:
|
|
7
|
+
# - CSS hot-reloading (without full page refresh)
|
|
8
|
+
# - Full page reload on JS/image changes
|
|
9
|
+
#
|
|
10
|
+
# Only outputs content when BunBunBundle.development? is true.
|
|
11
|
+
#
|
|
12
|
+
module ReloadTag
|
|
13
|
+
# Returns the live reload <script> tag, or an empty string in production.
|
|
14
|
+
#
|
|
15
|
+
# Example (ERB):
|
|
16
|
+
#
|
|
17
|
+
# <%= bun_reload_tag %>
|
|
18
|
+
#
|
|
19
|
+
def bun_reload_tag # rubocop:disable Metrics/MethodLength
|
|
20
|
+
return '' unless BunBunBundle.development?
|
|
21
|
+
|
|
22
|
+
config = BunBunBundle.config
|
|
23
|
+
css_paths = BunBunBundle.manifest.css_entry_points.map do |key|
|
|
24
|
+
"#{config.public_path}/#{key}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
<<~HTML
|
|
28
|
+
<script>
|
|
29
|
+
(() => {
|
|
30
|
+
const cssPaths = #{css_paths.to_json};
|
|
31
|
+
const ws = new WebSocket('#{config.dev_server.ws_url}')
|
|
32
|
+
|
|
33
|
+
ws.onmessage = (event) => {
|
|
34
|
+
const data = JSON.parse(event.data)
|
|
35
|
+
|
|
36
|
+
if (data.type === 'css') {
|
|
37
|
+
document.querySelectorAll('link[rel="stylesheet"]').forEach(link => {
|
|
38
|
+
const linkPath = new URL(link.href).pathname.split('?')[0]
|
|
39
|
+
if (cssPaths.some(p => linkPath.startsWith(p))) {
|
|
40
|
+
const url = new URL(link.href)
|
|
41
|
+
url.searchParams.set('r', Date.now())
|
|
42
|
+
link.href = url.toString()
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
console.log('\\u25b8 CSS reloaded')
|
|
46
|
+
} else if (data.type === 'error') {
|
|
47
|
+
console.error('\\u2716 Build error:', data.message)
|
|
48
|
+
} else {
|
|
49
|
+
console.log('\\u25b8 Reloading...')
|
|
50
|
+
location.reload()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
ws.onopen = () => console.log('\\u25b8 Live reload connected')
|
|
55
|
+
ws.onclose = () => setTimeout(() => location.reload(), 2000)
|
|
56
|
+
})()
|
|
57
|
+
</script>
|
|
58
|
+
HTML
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'bun_bun_bundle/version'
|
|
4
|
+
require_relative 'bun_bun_bundle/config'
|
|
5
|
+
require_relative 'bun_bun_bundle/manifest'
|
|
6
|
+
require_relative 'bun_bun_bundle/helpers'
|
|
7
|
+
require_relative 'bun_bun_bundle/reload_tag'
|
|
8
|
+
require_relative 'bun_bun_bundle/dev_cache_middleware'
|
|
9
|
+
|
|
10
|
+
module BunBunBundle
|
|
11
|
+
class << self
|
|
12
|
+
attr_writer :config, :manifest, :asset_host, :environment
|
|
13
|
+
|
|
14
|
+
def config
|
|
15
|
+
@config ||= Config.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def manifest
|
|
19
|
+
@manifest ||= Manifest.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def asset_host
|
|
23
|
+
@asset_host || ''
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def environment
|
|
27
|
+
@environment || ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def development?
|
|
31
|
+
environment == 'development'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def production?
|
|
35
|
+
environment == 'production'
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Returns the path to the bundled JS files shipped with the gem.
|
|
39
|
+
def bun_path
|
|
40
|
+
File.expand_path('bun', __dir__)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Resets all state. Useful for testing.
|
|
44
|
+
def reset!
|
|
45
|
+
@config = nil
|
|
46
|
+
@manifest = nil
|
|
47
|
+
@asset_host = nil
|
|
48
|
+
@environment = nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
require_relative 'bun_bun_bundle/railtie' if defined?(Rails::Railtie)
|
metadata
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: bun_bun_bundle
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Wout Fierens
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: json
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
description: Zero-dependency asset bundler with CSS hot-reloading, fingerprinting,
|
|
27
|
+
live reload, and a flexible plugin system. Works with Rails, Hanami, or any Rack
|
|
28
|
+
app.
|
|
29
|
+
executables:
|
|
30
|
+
- bun_bun_bundle
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- LICENSE
|
|
35
|
+
- README.md
|
|
36
|
+
- exe/bun_bun_bundle
|
|
37
|
+
- lib/bun/bake.js
|
|
38
|
+
- lib/bun/bun_bundle.js
|
|
39
|
+
- lib/bun/plugins/cssAliases.js
|
|
40
|
+
- lib/bun/plugins/cssGlobs.js
|
|
41
|
+
- lib/bun/plugins/index.js
|
|
42
|
+
- lib/bun/plugins/jsGlobs.js
|
|
43
|
+
- lib/bun_bun_bundle.rb
|
|
44
|
+
- lib/bun_bun_bundle/config.rb
|
|
45
|
+
- lib/bun_bun_bundle/dev_cache_middleware.rb
|
|
46
|
+
- lib/bun_bun_bundle/hanami.rb
|
|
47
|
+
- lib/bun_bun_bundle/helpers.rb
|
|
48
|
+
- lib/bun_bun_bundle/manifest.rb
|
|
49
|
+
- lib/bun_bun_bundle/railtie.rb
|
|
50
|
+
- lib/bun_bun_bundle/reload_tag.rb
|
|
51
|
+
- lib/bun_bun_bundle/version.rb
|
|
52
|
+
homepage: https://codeberg.org/w0u7/bun_bun_bundle
|
|
53
|
+
licenses:
|
|
54
|
+
- MIT
|
|
55
|
+
metadata:
|
|
56
|
+
homepage_uri: https://codeberg.org/w0u7/bun_bun_bundle
|
|
57
|
+
source_code_uri: https://codeberg.org/w0u7/bun_bun_bundle
|
|
58
|
+
changelog_uri: https://codeberg.org/w0u7/bun_bun_bundle/blob/main/CHANGELOG.md
|
|
59
|
+
rubygems_mfa_required: 'true'
|
|
60
|
+
rdoc_options: []
|
|
61
|
+
require_paths:
|
|
62
|
+
- lib
|
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: 3.1.0
|
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '0'
|
|
73
|
+
requirements: []
|
|
74
|
+
rubygems_version: 4.0.9
|
|
75
|
+
specification_version: 4
|
|
76
|
+
summary: A self-contained asset bundler powered by Bun
|
|
77
|
+
test_files: []
|