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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1acf2b909cdf00c5e5533943ad600811ee2cdd0fcd53885ca3e007f94b12db91
|
|
4
|
+
data.tar.gz: ee34bf6fd021e4470a9cb9f14ea7436f8b8eaf312229ad59724e4bb14a30afc1
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3ee9e73c7e1de4a56d70411f8ba49d755a09b7c4148fa220eeef75a2dd460f141ea57fe194c29df26f2bc68cbd9d11339941f62f130bfeb7fbf22f8e0c80994f
|
|
7
|
+
data.tar.gz: 835bcd2d1b8203f6497931e3d92ca23a2d428fe84c750bc86eb508364805b4cae738b0fe32c30362be8a35e4dcdabea891f43b098f48de496f9585e8d9d06917
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wout Fierens
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# BunBunBundle
|
|
2
|
+
|
|
3
|
+
A self-contained asset bundler for Ruby powered by [Bun](https://bun.sh). No
|
|
4
|
+
development dependencies, no complex configuration. Fast builds with CSS
|
|
5
|
+
hot-reloading, fingerprinting, live reload, and a flexible plugin system. Works
|
|
6
|
+
with Rails, Hanami, or any Rack app.
|
|
7
|
+
|
|
8
|
+
## Why use BunBunBundle?
|
|
9
|
+
|
|
10
|
+
### Lightning fast bundling
|
|
11
|
+
|
|
12
|
+
BunBunBundle leverages Bun's native bundler which is orders of magnitude faster
|
|
13
|
+
than traditional Node.js-based tools. Your assets are built in milliseconds,
|
|
14
|
+
not seconds.
|
|
15
|
+
|
|
16
|
+
### CSS hot-reloading
|
|
17
|
+
|
|
18
|
+
CSS changes are hot-reloaded in the browser without a full page refresh. Your
|
|
19
|
+
state stays intact, your scroll position is preserved, and you see changes
|
|
20
|
+
instantly.
|
|
21
|
+
|
|
22
|
+
### Asset fingerprinting
|
|
23
|
+
|
|
24
|
+
Every asset is fingerprinted with a content-based hash in production, so
|
|
25
|
+
browsers always fetch the right version.
|
|
26
|
+
|
|
27
|
+
### No surprises in production
|
|
28
|
+
|
|
29
|
+
Development and production builds go through the exact same pipeline. The only
|
|
30
|
+
differences are fingerprinting and minification being enabled in production,
|
|
31
|
+
but nothing is holding you back form them in development as well.
|
|
32
|
+
|
|
33
|
+
### Extensible plugin system
|
|
34
|
+
|
|
35
|
+
Comes with built-in plugins for CSS glob imports, root aliases, and JS glob
|
|
36
|
+
imports. Plugins are simple, plain JS files, so you can create your own JS/CSS
|
|
37
|
+
transformers, and raw Bun plugins are supported as well.
|
|
38
|
+
|
|
39
|
+
### Just one dependency: Bun
|
|
40
|
+
|
|
41
|
+
The bundler ships with the gem. Bun is the only external requirement, so there
|
|
42
|
+
are zero dev dependencies.
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
1. Add the gem to your `Gemfile`:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
gem 'bun_bun_bundle'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
2. Run `bundle install`
|
|
53
|
+
|
|
54
|
+
3. Make sure [Bun](https://bun.sh) is installed:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
curl -fsSL https://bun.sh/install | bash
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Usage with Rails
|
|
61
|
+
|
|
62
|
+
The gem auto-configures itself through a Railtie. All helpers are available in
|
|
63
|
+
your views immediately:
|
|
64
|
+
|
|
65
|
+
```erb
|
|
66
|
+
<!DOCTYPE html>
|
|
67
|
+
<html>
|
|
68
|
+
<head>
|
|
69
|
+
<%= bun_css_tag('css/app.css') %>
|
|
70
|
+
</head>
|
|
71
|
+
<body>
|
|
72
|
+
<%= bun_img_tag('images/logo.png', alt: 'My App') %>
|
|
73
|
+
<%= bun_js_tag('js/app.js', defer: true) %>
|
|
74
|
+
<%= bun_reload_tag %>
|
|
75
|
+
</body>
|
|
76
|
+
</html>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The `DevCacheMiddleware` is automatically inserted in development to prevent
|
|
80
|
+
stale asset caching.
|
|
81
|
+
|
|
82
|
+
## Usage with Hanami
|
|
83
|
+
|
|
84
|
+
1. Require the Hanami integration:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
# config/app.rb
|
|
88
|
+
|
|
89
|
+
require 'bun_bun_bundle/hanami'
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
2. Optionally add the dev cache middleware:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
# config/app.rb
|
|
96
|
+
|
|
97
|
+
module MyApp
|
|
98
|
+
class App < Hanami::App
|
|
99
|
+
config.middleware.use BunBunBundle::DevCacheMiddleware if Hanami.env?(:development)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
3. Include the helpers in your views:
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
# app/views/helpers.rb
|
|
108
|
+
|
|
109
|
+
module MyApp
|
|
110
|
+
module Views
|
|
111
|
+
module Helpers
|
|
112
|
+
include BunBunBundle::Helpers
|
|
113
|
+
include BunBunBundle::ReloadTag
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
4. Use them in your templates:
|
|
120
|
+
|
|
121
|
+
```erb
|
|
122
|
+
<%= bun_css_tag('css/app.css') %>
|
|
123
|
+
<%= bun_js_tag('js/app.js') %>
|
|
124
|
+
<%= bun_reload_tag %>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Usage with any Rack app
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
require 'bun_bun_bundle'
|
|
131
|
+
|
|
132
|
+
# Configure manually
|
|
133
|
+
BunBunBundle.config = BunBunBundle::Config.load(root: __dir__)
|
|
134
|
+
BunBunBundle.manifest = BunBunBundle::Manifest.load(root: __dir__)
|
|
135
|
+
|
|
136
|
+
# Optionally set a CDN host
|
|
137
|
+
BunBunBundle.asset_host = 'https://cdn.example.com'
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Helpers
|
|
141
|
+
|
|
142
|
+
All helpers are prefixed with `bun_` to avoid conflicts with framework helpers:
|
|
143
|
+
|
|
144
|
+
| Helper | Description |
|
|
145
|
+
| -------------------------------- | ------------------------------------------------ |
|
|
146
|
+
| `bun_asset('images/logo.png')` | Returns the fingerprinted asset path |
|
|
147
|
+
| `bun_js_tag('js/app.js')` | Generates a `<script>` tag |
|
|
148
|
+
| `bun_css_tag('css/app.css')` | Generates a `<link>` tag |
|
|
149
|
+
| `bun_img_tag('images/logo.png')` | Generates an `<img>` tag |
|
|
150
|
+
| `bun_reload_tag` | Live reload script (only renders in development) |
|
|
151
|
+
|
|
152
|
+
All tag helpers accept additional HTML attributes:
|
|
153
|
+
|
|
154
|
+
```erb
|
|
155
|
+
<%= bun_js_tag('js/app.js', defer: true, async: true) %>
|
|
156
|
+
<%= bun_css_tag('css/app.css', media: 'print') %>
|
|
157
|
+
<%= bun_img_tag('images/logo.png', alt: 'My App', class: 'logo') %>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## CLI
|
|
161
|
+
|
|
162
|
+
Build your assets using the bundled CLI:
|
|
163
|
+
|
|
164
|
+
```sh
|
|
165
|
+
# Development: builds, watches, and starts the live reload server
|
|
166
|
+
bun_bun_bundle dev
|
|
167
|
+
|
|
168
|
+
# Production: builds with fingerprinting and minification
|
|
169
|
+
bun_bun_bundle build
|
|
170
|
+
|
|
171
|
+
# Development with a production build (fingerprinting + minification)
|
|
172
|
+
bun_bun_bundle dev --prod
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Configuration
|
|
176
|
+
|
|
177
|
+
Place a `config/bun.json` in your project root:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"entryPoints": {
|
|
182
|
+
"js": ["app/assets/js/app.js"],
|
|
183
|
+
"css": ["app/assets/css/app.css"]
|
|
184
|
+
},
|
|
185
|
+
"outDir": "public/assets",
|
|
186
|
+
"publicPath": "/assets",
|
|
187
|
+
"manifestPath": "public/bun-manifest.json",
|
|
188
|
+
"staticDirs": ["app/assets/images", "app/assets/fonts"],
|
|
189
|
+
"devServer": {
|
|
190
|
+
"host": "127.0.0.1",
|
|
191
|
+
"port": 3002,
|
|
192
|
+
"secure": false
|
|
193
|
+
},
|
|
194
|
+
"plugins": {
|
|
195
|
+
"css": ["cssAliases", "cssGlobs"],
|
|
196
|
+
"js": ["jsGlobs"]
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
All values shown above are defaults, you only need to specify what you want to
|
|
202
|
+
override.
|
|
203
|
+
|
|
204
|
+
## Plugins
|
|
205
|
+
|
|
206
|
+
Three plugins are included out of the box:
|
|
207
|
+
|
|
208
|
+
| Plugin | Description |
|
|
209
|
+
| ------------ | ---------------------------------------------------------------- |
|
|
210
|
+
| `cssAliases` | Resolves `$/` root aliases in CSS `url()` references |
|
|
211
|
+
| `cssGlobs` | Expands glob patterns in `@import` statements |
|
|
212
|
+
| `jsGlobs` | Compiles `import x from 'glob:./path/*.js'` into object mappings |
|
|
213
|
+
|
|
214
|
+
### Custom plugins
|
|
215
|
+
|
|
216
|
+
Create a JS file that exports a factory function:
|
|
217
|
+
|
|
218
|
+
```javascript
|
|
219
|
+
// config/bun/banner.js
|
|
220
|
+
|
|
221
|
+
export default function banner({ prod }) {
|
|
222
|
+
return (content) => {
|
|
223
|
+
const stamp = prod ? "" : ` (dev build ${new Date().toISOString()})`;
|
|
224
|
+
return `/* My App${stamp} */\n${content}`;
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Then reference it in your config:
|
|
230
|
+
|
|
231
|
+
```json
|
|
232
|
+
{
|
|
233
|
+
"plugins": {
|
|
234
|
+
"css": ["cssAliases", "cssGlobs", "config/bun/banner.js"]
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Project structure
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
your-app/
|
|
243
|
+
├── app/
|
|
244
|
+
│ └── assets/
|
|
245
|
+
│ ├── css/
|
|
246
|
+
│ │ └── app.css # CSS entry point
|
|
247
|
+
│ ├── js/
|
|
248
|
+
│ │ └── app.js # JS entry point
|
|
249
|
+
│ ├── images/ # Static images (copied + fingerprinted)
|
|
250
|
+
│ └── fonts/ # Static fonts (copied + fingerprinted)
|
|
251
|
+
├── config/
|
|
252
|
+
│ └── bun.json # Optional bundler configuration
|
|
253
|
+
└── public/
|
|
254
|
+
├── assets/ # Built assets (generated)
|
|
255
|
+
└── bun-manifest.json # Asset manifest (generated)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Origins
|
|
259
|
+
|
|
260
|
+
BunBunBundle was originally built for [Fluck](https://fluck.site), a
|
|
261
|
+
self-hostable website builder using [Lucky
|
|
262
|
+
Framework](https://luckyframework.org/). I wanted to have a fast, comprehensive
|
|
263
|
+
asset bundler that would not require too much maintenance in the long term.
|
|
264
|
+
|
|
265
|
+
Bun was the natural choice because it does almost everything:
|
|
266
|
+
|
|
267
|
+
- JS bundling, tree-shaking, and minification
|
|
268
|
+
- CSS processing and minification (through the built-in
|
|
269
|
+
[LightningCSS](https://lightningcss.dev/) library)
|
|
270
|
+
- WebSocket server for hot and live reloading
|
|
271
|
+
- Content hashing for asset fingerprints
|
|
272
|
+
- Extendability with simple plugins
|
|
273
|
+
|
|
274
|
+
It's also fast and reliable. We use this setup heavily in two Lucky apps and it
|
|
275
|
+
is rock solid, and it has since been adopted by Lucky as the default builder.
|
|
276
|
+
|
|
277
|
+
I wanted to have the same setup in my Ruby apps as well, that's when this Gem
|
|
278
|
+
was born. I hope you enjoy it too!
|
|
279
|
+
|
|
280
|
+
## Contributing
|
|
281
|
+
|
|
282
|
+
We use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/).
|
|
283
|
+
|
|
284
|
+
1. Fork it
|
|
285
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
286
|
+
3. Commit your changes (`git commit -am 'feat: new feature'`)
|
|
287
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
288
|
+
5. Create a new Pull Request
|
|
289
|
+
|
|
290
|
+
## Contributors
|
|
291
|
+
|
|
292
|
+
- [Wout](https://codeberg.org/w0u7) - creator and maintainer
|
|
293
|
+
|
|
294
|
+
## License
|
|
295
|
+
|
|
296
|
+
[MIT](LICENSE)
|
data/exe/bun_bun_bundle
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'bun_bun_bundle'
|
|
5
|
+
|
|
6
|
+
command = ARGV[0]
|
|
7
|
+
flags = ARGV[1..]
|
|
8
|
+
bun_dir = BunBunBundle.bun_path
|
|
9
|
+
bake = File.join(bun_dir, 'bake.js')
|
|
10
|
+
|
|
11
|
+
case command
|
|
12
|
+
when 'dev', 'watch'
|
|
13
|
+
exec('bun', 'run', bake, '--dev', *flags)
|
|
14
|
+
when 'build'
|
|
15
|
+
exec('bun', 'run', bake, '--prod', *flags)
|
|
16
|
+
else
|
|
17
|
+
puts <<~USAGE
|
|
18
|
+
Usage: bun_bun_bundle <command> [flags]
|
|
19
|
+
|
|
20
|
+
Commands:
|
|
21
|
+
dev Start development server with live reload and CSS hot-reloading
|
|
22
|
+
build Build assets for production (with fingerprinting)
|
|
23
|
+
|
|
24
|
+
Flags:
|
|
25
|
+
--prod Enable fingerprinting and minification
|
|
26
|
+
--dev Enable source maps and watch mode
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
bun_bun_bundle dev # development with live reload
|
|
30
|
+
bun_bun_bundle dev --prod # development with production build
|
|
31
|
+
bun_bun_bundle build # production build
|
|
32
|
+
|
|
33
|
+
Configuration:
|
|
34
|
+
Place a config/bun.json in your project root to customize settings.
|
|
35
|
+
See the README for available options.
|
|
36
|
+
USAGE
|
|
37
|
+
exit(command ? 1 : 0)
|
|
38
|
+
end
|
data/lib/bun/bake.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import {mkdirSync, readFileSync, existsSync, rmSync, watch} from 'fs'
|
|
2
|
+
import {join, dirname, basename, extname} from 'path'
|
|
3
|
+
import {Glob} from 'bun'
|
|
4
|
+
import {resolvePlugins} from './plugins/index.js'
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
CONFIG_PATH: 'config/bun.json',
|
|
8
|
+
IGNORE_PATTERNS: [
|
|
9
|
+
/^\d+$/,
|
|
10
|
+
/^\.#/,
|
|
11
|
+
/~$/,
|
|
12
|
+
/\.swp$/,
|
|
13
|
+
/\.swo$/,
|
|
14
|
+
/\.tmp$/,
|
|
15
|
+
/^#.*#$/,
|
|
16
|
+
/\.DS_Store$/
|
|
17
|
+
],
|
|
18
|
+
|
|
19
|
+
root: process.cwd(),
|
|
20
|
+
config: null,
|
|
21
|
+
manifest: {},
|
|
22
|
+
dev: false,
|
|
23
|
+
prod: false,
|
|
24
|
+
wsClients: new Set(),
|
|
25
|
+
plugins: [],
|
|
26
|
+
|
|
27
|
+
flags({dev, prod}) {
|
|
28
|
+
if (dev != null) this.dev = dev
|
|
29
|
+
if (prod != null) this.prod = prod
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
deepMerge(target, source) {
|
|
33
|
+
const result = {...target}
|
|
34
|
+
for (const k of Object.keys(source))
|
|
35
|
+
result[k] =
|
|
36
|
+
source[k] && typeof source[k] === 'object' && !Array.isArray(source[k])
|
|
37
|
+
? this.deepMerge(target[k] || {}, source[k])
|
|
38
|
+
: source[k]
|
|
39
|
+
return result
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
loadConfig() {
|
|
43
|
+
const defaults = {
|
|
44
|
+
entryPoints: {js: ['app/assets/js/app.js'], css: ['app/assets/css/app.css']},
|
|
45
|
+
plugins: {css: ['cssAliases', 'cssGlobs'], js: ['jsGlobs']},
|
|
46
|
+
staticDirs: ['app/assets/images', 'app/assets/fonts'],
|
|
47
|
+
outDir: 'public/assets',
|
|
48
|
+
publicPath: '/assets',
|
|
49
|
+
manifestPath: 'public/bun-manifest.json',
|
|
50
|
+
devServer: {host: '127.0.0.1', port: 3002, secure: false}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const json = readFileSync(join(this.root, this.CONFIG_PATH), 'utf-8')
|
|
55
|
+
const user = JSON.parse(json)
|
|
56
|
+
this.config = this.deepMerge(defaults, user)
|
|
57
|
+
if (user.plugins != null) this.config.plugins = user.plugins
|
|
58
|
+
} catch {
|
|
59
|
+
this.config = defaults
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
async loadPlugins() {
|
|
64
|
+
this.plugins = await resolvePlugins(this.config.plugins, {
|
|
65
|
+
root: this.root,
|
|
66
|
+
config: this.config,
|
|
67
|
+
dev: this.dev,
|
|
68
|
+
prod: this.prod,
|
|
69
|
+
manifest: this.manifest
|
|
70
|
+
})
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
get outDir() {
|
|
74
|
+
if (this.config == null) throw new Error(' ✖ Config is not loaded')
|
|
75
|
+
|
|
76
|
+
return join(this.root, this.config.outDir)
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
fingerprint(name, ext, content) {
|
|
80
|
+
if (!this.prod) return `${name}${ext}`
|
|
81
|
+
|
|
82
|
+
const hash = Bun.hash(content).toString(16).slice(0, 8)
|
|
83
|
+
return `${name}-${hash}${ext}`
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
async buildAssets(type, options = {}) {
|
|
87
|
+
const outDir = join(this.outDir, type)
|
|
88
|
+
mkdirSync(outDir, {recursive: true})
|
|
89
|
+
|
|
90
|
+
const entries = this.config.entryPoints[type]
|
|
91
|
+
const ext = `.${type}`
|
|
92
|
+
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
const entryPath = join(this.root, entry)
|
|
95
|
+
const entryName = basename(entry).replace(/\.(ts|js|tsx|jsx|css)$/, '')
|
|
96
|
+
|
|
97
|
+
if (!existsSync(entryPath)) {
|
|
98
|
+
console.warn(` ▸ Missing entry point ${entry}, continuing...`)
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const result = await Bun.build({
|
|
103
|
+
entrypoints: [entryPath],
|
|
104
|
+
minify: this.prod,
|
|
105
|
+
plugins: this.plugins,
|
|
106
|
+
...options
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
if (!result.success) {
|
|
110
|
+
console.error(` ▸ Failed to build ${entry}`)
|
|
111
|
+
for (const log of result.logs) console.error(log)
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const output = result.outputs.find(o => o.path.endsWith(ext))
|
|
116
|
+
if (!output) {
|
|
117
|
+
console.error(` ▸ No ${type.toUpperCase()} output for ${entry}`)
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const content = await output.text()
|
|
122
|
+
const fileName = this.fingerprint(entryName, ext, content)
|
|
123
|
+
await Bun.write(join(outDir, fileName), content)
|
|
124
|
+
|
|
125
|
+
this.manifest[`${type}/${entryName}${ext}`] = `${type}/${fileName}`
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
async buildJS() {
|
|
130
|
+
await this.buildAssets('js', {
|
|
131
|
+
target: 'browser',
|
|
132
|
+
format: 'iife',
|
|
133
|
+
sourcemap: this.dev ? 'inline' : 'none'
|
|
134
|
+
})
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
async buildCSS() {
|
|
138
|
+
await this.buildAssets('css')
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
async copyStaticAssets() {
|
|
142
|
+
const glob = new Glob('**/*.*')
|
|
143
|
+
|
|
144
|
+
for (const dir of this.config.staticDirs) {
|
|
145
|
+
const fullDir = join(this.root, dir)
|
|
146
|
+
if (!existsSync(fullDir)) continue
|
|
147
|
+
|
|
148
|
+
const assetType = basename(dir)
|
|
149
|
+
const destDir = join(this.outDir, assetType)
|
|
150
|
+
|
|
151
|
+
for await (const file of glob.scan({cwd: fullDir, onlyFiles: true})) {
|
|
152
|
+
const srcPath = join(fullDir, file)
|
|
153
|
+
const content = await Bun.file(srcPath).arrayBuffer()
|
|
154
|
+
|
|
155
|
+
const ext = extname(file)
|
|
156
|
+
const name = file.slice(0, -ext.length) || file
|
|
157
|
+
const fileName = this.fingerprint(name, ext, new Uint8Array(content))
|
|
158
|
+
const destPath = join(destDir, fileName)
|
|
159
|
+
|
|
160
|
+
mkdirSync(dirname(destPath), {recursive: true})
|
|
161
|
+
await Bun.write(destPath, content)
|
|
162
|
+
|
|
163
|
+
this.manifest[`${assetType}/${file}`] = `${assetType}/${fileName}`
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
cleanOutDir() {
|
|
169
|
+
rmSync(this.outDir, {recursive: true, force: true})
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
async writeManifest() {
|
|
173
|
+
const manifestFullPath = join(this.root, this.config.manifestPath)
|
|
174
|
+
mkdirSync(dirname(manifestFullPath), {recursive: true})
|
|
175
|
+
await Bun.write(manifestFullPath, JSON.stringify(this.manifest, null, 2))
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
async build() {
|
|
179
|
+
const env = this.prod ? 'production' : 'development'
|
|
180
|
+
console.log(`Building manifest for ${env}...`)
|
|
181
|
+
const start = performance.now()
|
|
182
|
+
this.loadConfig()
|
|
183
|
+
await this.loadPlugins()
|
|
184
|
+
this.cleanOutDir()
|
|
185
|
+
await this.copyStaticAssets()
|
|
186
|
+
await this.buildJS()
|
|
187
|
+
await this.buildCSS()
|
|
188
|
+
await this.writeManifest()
|
|
189
|
+
const ms = Math.round(performance.now() - start)
|
|
190
|
+
console.log(`DONE Built successfully in ${ms} ms`, this.prettyManifest())
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
prettyManifest() {
|
|
194
|
+
const lines = Object.entries(this.manifest)
|
|
195
|
+
.map(([key, value]) => ` ${key} → ${value}`)
|
|
196
|
+
.join('\n')
|
|
197
|
+
return `\n${lines}\n\n`
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
reload(type = 'full') {
|
|
201
|
+
setTimeout(() => {
|
|
202
|
+
const message = JSON.stringify({type})
|
|
203
|
+
for (const client of this.wsClients) {
|
|
204
|
+
try {
|
|
205
|
+
client.send(message)
|
|
206
|
+
} catch {
|
|
207
|
+
this.wsClients.delete(client)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}, 50)
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
async watch() {
|
|
214
|
+
const srcDir = join(this.root, this.config.watchDir || 'app/assets')
|
|
215
|
+
|
|
216
|
+
watch(srcDir, {recursive: true}, async (event, filename) => {
|
|
217
|
+
if (!filename) return
|
|
218
|
+
|
|
219
|
+
const normalizedFilename = filename.replace(/\\/g, '/')
|
|
220
|
+
const base = basename(normalizedFilename)
|
|
221
|
+
const ext = extname(base).slice(1)
|
|
222
|
+
|
|
223
|
+
if (this.IGNORE_PATTERNS.some(pattern => pattern.test(base))) return
|
|
224
|
+
|
|
225
|
+
console.log(` ▸ ${normalizedFilename} changed`)
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
if (ext === 'css') await this.buildCSS()
|
|
229
|
+
else if (['js', 'ts', 'jsx', 'tsx'].includes(ext)) await this.buildJS()
|
|
230
|
+
else if (base.includes('.')) await this.copyStaticAssets()
|
|
231
|
+
|
|
232
|
+
await this.writeManifest()
|
|
233
|
+
this.reload(ext === 'css' ? 'css' : 'full')
|
|
234
|
+
} catch (err) {
|
|
235
|
+
console.error(' ✖ Build error:', err.message)
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
console.log('Beginning to watch your project')
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
async serve() {
|
|
243
|
+
await this.build()
|
|
244
|
+
await this.watch()
|
|
245
|
+
|
|
246
|
+
const {host, port, secure} = this.config.devServer
|
|
247
|
+
const wsClients = this.wsClients
|
|
248
|
+
|
|
249
|
+
Bun.serve({
|
|
250
|
+
hostname: secure ? '0.0.0.0' : host,
|
|
251
|
+
port,
|
|
252
|
+
fetch(req, server) {
|
|
253
|
+
if (server.upgrade(req)) return
|
|
254
|
+
return new Response('BunBunBundle WebSocket Server', {status: 200})
|
|
255
|
+
},
|
|
256
|
+
websocket: {
|
|
257
|
+
open(ws) {
|
|
258
|
+
wsClients.add(ws)
|
|
259
|
+
console.log(` ▸ Client connected (${wsClients.size})\n\n`)
|
|
260
|
+
},
|
|
261
|
+
close(ws) {
|
|
262
|
+
wsClients.delete(ws)
|
|
263
|
+
console.log(` ▸ Client disconnected (${wsClients.size})\n\n`)
|
|
264
|
+
},
|
|
265
|
+
message() {}
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
const protocol = secure ? 'wss' : 'ws'
|
|
270
|
+
console.log(`\n\n 🔌 Live reload at ${protocol}://${host}:${port}\n\n`)
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
async bake() {
|
|
274
|
+
this.dev ? await this.serve() : await this.build()
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {join} from 'path'
|
|
2
|
+
|
|
3
|
+
const REGEX = /url\(\s*['"]?\$\//g
|
|
4
|
+
|
|
5
|
+
// Resolves `$` root aliases in CSS url() references.
|
|
6
|
+
// e.g. url('$/images/foo.png') → url('/absolute/src/images/foo.png')
|
|
7
|
+
export default function cssAliases({root}) {
|
|
8
|
+
const srcDir = join(root, 'src')
|
|
9
|
+
return content => content.replace(REGEX, `url('${srcDir}/`)
|
|
10
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {dirname, relative, resolve, join} from 'path'
|
|
2
|
+
import {Glob} from 'bun'
|
|
3
|
+
|
|
4
|
+
const REGEX = /@import\s+['"]([^'"]*\*[^'"]*)['"]\s*;/g
|
|
5
|
+
|
|
6
|
+
// Expands glob patterns in CSS @import statements.
|
|
7
|
+
// e.g. @import './components/**/*.css' → individual @import lines.
|
|
8
|
+
export default function cssGlobs() {
|
|
9
|
+
return async (content, args) => {
|
|
10
|
+
const fileDir = dirname(args.path)
|
|
11
|
+
const replacements = []
|
|
12
|
+
|
|
13
|
+
for (const [fullMatch, pattern] of content.matchAll(REGEX)) {
|
|
14
|
+
const lastSlash = pattern.lastIndexOf('/', pattern.indexOf('*'))
|
|
15
|
+
const basePath = lastSlash > 0 ? pattern.slice(0, lastSlash) : '.'
|
|
16
|
+
const baseDir = resolve(fileDir, basePath)
|
|
17
|
+
const glob = new Glob(pattern.slice(lastSlash + 1))
|
|
18
|
+
const files = []
|
|
19
|
+
|
|
20
|
+
for await (const file of glob.scan({cwd: baseDir, onlyFiles: true})) {
|
|
21
|
+
const absPath = join(baseDir, file)
|
|
22
|
+
if (absPath === args.path) continue
|
|
23
|
+
const relPath = relative(fileDir, absPath)
|
|
24
|
+
files.push(relPath.startsWith('.') ? relPath : `./${relPath}`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
files.sort()
|
|
28
|
+
|
|
29
|
+
if (!files.length) console.warn(` CSS glob matched no files: ${pattern}`)
|
|
30
|
+
|
|
31
|
+
replacements.push({
|
|
32
|
+
fullMatch,
|
|
33
|
+
expanded: files.map(f => `@import '${f}';`).join('\n'),
|
|
34
|
+
count: files.length,
|
|
35
|
+
pattern
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const {fullMatch, expanded, count, pattern} of replacements) {
|
|
40
|
+
const s = count !== 1 ? 's' : ''
|
|
41
|
+
console.log(` CSS glob: ${pattern} → ${count} file${s}`)
|
|
42
|
+
content = content.replace(fullMatch, expanded)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return content
|
|
46
|
+
}
|
|
47
|
+
}
|