From 9fb7d4e177635a057eef754aef923f90e4c919a1 Mon Sep 17 00:00:00 2001 From: Wout Date: Sat, 4 Apr 2026 08:45:41 +0200 Subject: [PATCH 1/9] Accept arrays and strings for js/css entrypoints in bun config --- spec/bun/lucky.test.js | 10 ++++++++++ src/bun/lucky.js | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/spec/bun/lucky.test.js b/spec/bun/lucky.test.js index c10dc13bd..204eeef5a 100644 --- a/spec/bun/lucky.test.js +++ b/spec/bun/lucky.test.js @@ -187,6 +187,16 @@ describe('buildAssets', () => { expect(LuckyBun.manifest['js/app.js']).toBeUndefined() }) + test('accepts a string entry point', async () => { + await setupProject( + {'src/js/app.js': 'console.log("single")'}, + {entryPoints: {js: 'src/js/app.js'}} + ) + await LuckyBun.buildJS() + + expect(LuckyBun.manifest['js/app.js']).toBe('js/app.js') + }) + test('builds multiple JS entry points', async () => { await buildJS( { diff --git a/src/bun/lucky.js b/src/bun/lucky.js index 33382eaba..bfc1e0e03 100644 --- a/src/bun/lucky.js +++ b/src/bun/lucky.js @@ -87,7 +87,8 @@ export default { const outDir = join(this.outDir, type) mkdirSync(outDir, {recursive: true}) - const entries = this.config.entryPoints[type] + const raw = this.config.entryPoints[type] + const entries = Array.isArray(raw) ? raw : [raw] const ext = `.${type}` for (const entry of entries) { From 1f04efcfa29d0a18befaa62d5bcbbb90c7702713 Mon Sep 17 00:00:00 2001 From: Wout Date: Sat, 4 Apr 2026 09:02:26 +0200 Subject: [PATCH 2/9] Allow string entrpypoints in Bun config on the Crystal side --- src/bun/config.cr | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/bun/config.cr b/src/bun/config.cr index 2e6124d1d..9c8eba9a4 100644 --- a/src/bun/config.cr +++ b/src/bun/config.cr @@ -27,10 +27,26 @@ module LuckyBun struct EntryPoints include JSON::Serializable + @[JSON::Field(converter: LuckyBun::Config::StringOrArray)] getter js : Array(String) = %w[src/js/app.js] + + @[JSON::Field(converter: LuckyBun::Config::StringOrArray)] getter css : Array(String) = %w[src/css/app.css] end + module StringOrArray + def self.from_json(pull : JSON::PullParser) : Array(String) + case pull.kind + when .string? then [pull.read_string] + else Array(String).new(pull) + end + end + + def self.to_json(value : Array(String), json : JSON::Builder) : Nil + value.to_json(json) + end + end + struct DevServer include JSON::Serializable From 1f5ba2fda0ea7d0a594843e4c87f3b04d2628ebe Mon Sep 17 00:00:00 2001 From: Wout Date: Sat, 4 Apr 2026 09:02:51 +0200 Subject: [PATCH 3/9] Add basic test suite for the Bun config on the Crystal side --- spec/bun/config_spec.cr | 42 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 spec/bun/config_spec.cr diff --git a/spec/bun/config_spec.cr b/spec/bun/config_spec.cr new file mode 100644 index 000000000..6f4283e5b --- /dev/null +++ b/spec/bun/config_spec.cr @@ -0,0 +1,42 @@ +require "../spec_helper" + +describe LuckyBun::Config do + it "uses defaults without a config file" do + config = LuckyBun::Config.from_json("{}") + + config.dev_server.host.should eq("127.0.0.1") + config.dev_server.port.should eq(3002) + config.dev_server.secure?.should be_false + config.dev_server.ws_url.should eq("ws://127.0.0.1:3002") + config.entry_points.css.should eq(%w[src/css/app.css]) + config.entry_points.js.should eq(%w[src/js/app.js]) + config.manifest_path.should eq("public/bun-manifest.json") + config.out_dir.should eq("public/assets") + config.public_path.should eq("/assets") + config.static_dirs.should eq(%w[src/images src/fonts]) + end + + it "accepts string entry points" do + config = LuckyBun::Config.from_json( + %({"entryPoints": {"js": "src/js/app.ts", "css": "src/css/app.css"}}) + ) + + config.entry_points.js.should eq(%w[src/js/app.ts]) + config.entry_points.css.should eq(%w[src/css/app.css]) + end + + it "accepts array entry points" do + config = LuckyBun::Config.from_json( + %({"entryPoints": {"js": ["src/js/app.js", "src/js/admin.js"]}}) + ) + + config.entry_points.js.should eq(%w[src/js/app.js src/js/admin.js]) + end + + it "supports secure websocket url" do + config = LuckyBun::Config.from_json(%({"devServer": {"secure": true}})) + + config.dev_server.ws_protocol.should eq("wss") + config.dev_server.ws_url.should eq("wss://127.0.0.1:3002") + end +end From ad9dd582495d33bb9e12a8b960cfe9869da30627 Mon Sep 17 00:00:00 2001 From: Wout Date: Sat, 4 Apr 2026 09:32:05 +0200 Subject: [PATCH 4/9] Add `--debug` flag to enabel WebSocket client connection output --- spec/bun/lucky.test.js | 4 ++++ src/bun/bake.js | 1 + src/bun/lucky.js | 9 ++++++--- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/spec/bun/lucky.test.js b/spec/bun/lucky.test.js index 204eeef5a..4411ad424 100644 --- a/spec/bun/lucky.test.js +++ b/spec/bun/lucky.test.js @@ -11,6 +11,7 @@ beforeEach(() => { LuckyBun.manifest = {} LuckyBun.config = null LuckyBun.plugins = [] + LuckyBun.debug = false LuckyBun.prod = false LuckyBun.dev = false LuckyBun.root = TEST_DIR @@ -59,6 +60,9 @@ describe('flags', () => { LuckyBun.flags({prod: true}) expect(LuckyBun.prod).toBe(true) + LuckyBun.flags({debug: true}) + expect(LuckyBun.debug).toBe(true) + LuckyBun.dev = true LuckyBun.flags({prod: false}) expect(LuckyBun.dev).toBe(true) diff --git a/src/bun/bake.js b/src/bun/bake.js index 65f27fb8d..2e62ab630 100644 --- a/src/bun/bake.js +++ b/src/bun/bake.js @@ -1,6 +1,7 @@ import LuckyBun from './lucky.js' LuckyBun.flags({ + debug: process.argv.includes('--debug'), dev: process.argv.includes('--dev'), prod: process.argv.includes('--prod') }) diff --git a/src/bun/lucky.js b/src/bun/lucky.js index bfc1e0e03..0429f922d 100644 --- a/src/bun/lucky.js +++ b/src/bun/lucky.js @@ -18,13 +18,15 @@ export default { root: process.cwd(), config: null, manifest: {}, + debug: false, dev: false, prod: false, wsClients: new Set(), watchTimers: new Map(), plugins: [], - flags({dev, prod}) { + flags({debug, dev, prod}) { + if (debug != null) this.debug = debug if (dev != null) this.dev = dev if (prod != null) this.prod = prod }, @@ -270,6 +272,7 @@ export default { await this.watch() const {host, port, secure} = this.config.devServer + const debug = this.debug const wsClients = this.wsClients Bun.serve({ @@ -282,11 +285,11 @@ export default { websocket: { open(ws) { wsClients.add(ws) - console.log(` ▸ Client connected (${wsClients.size})\n\n`) + if (debug) console.log(` ▸ Client connected (${wsClients.size})\n\n`) }, close(ws) { wsClients.delete(ws) - console.log(` ▸ Client disconnected (${wsClients.size})\n\n`) + if (debug) console.log(` ▸ Client disconnected (${wsClients.size})\n\n`) }, message() {} } From e91e85a1db3b6b4a0d1778659e7926a33090a7c7 Mon Sep 17 00:00:00 2001 From: Wout Date: Sat, 4 Apr 2026 11:03:26 +0200 Subject: [PATCH 5/9] Fix eternal reconnect loop in `bun_reload_connect_tag` --- src/lucky/tags/bun_reload_tag.cr | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/lucky/tags/bun_reload_tag.cr b/src/lucky/tags/bun_reload_tag.cr index 5b2785261..1dd9ccc0d 100644 --- a/src/lucky/tags/bun_reload_tag.cr +++ b/src/lucky/tags/bun_reload_tag.cr @@ -13,6 +13,7 @@ module Lucky::BunReloadTag (() => { const cssPaths = #{bun_reload_connect_css_files(config).to_json}; const ws = new WebSocket('#{config.dev_server.ws_url}') + let connected = false ws.onmessage = (event) => { const data = JSON.parse(event.data) @@ -35,8 +36,13 @@ module Lucky::BunReloadTag } } - ws.onopen = () => console.log('▸ Live reload connected') - ws.onclose = () => setTimeout(() => location.reload(), 2000) + ws.onopen = () => { + connected = true + console.log('▸ Live reload connected') + } + ws.onclose = () => { + if (connected) setTimeout(() => location.reload(), 2000) + } })() JS end From 424c8310522dabea574a2ae2e533aaf4db691ee7 Mon Sep 17 00:00:00 2001 From: Wout Date: Sat, 4 Apr 2026 11:04:14 +0200 Subject: [PATCH 6/9] Add `listenHost` option to bun config for docker development setups --- spec/bun/lucky.test.js | 12 ++++++++++++ src/bun/lucky.js | 5 +++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/spec/bun/lucky.test.js b/spec/bun/lucky.test.js index 4411ad424..772128eb0 100644 --- a/spec/bun/lucky.test.js +++ b/spec/bun/lucky.test.js @@ -113,6 +113,18 @@ describe('loadConfig', () => { expect(LuckyBun.config.entryPoints.js).toEqual(['src/js/app.js']) }) + test('merges listenHost into devServer config', () => { + createFile( + 'config/bun.json', + JSON.stringify({devServer: {listenHost: '0.0.0.0'}}) + ) + + LuckyBun.loadConfig() + + expect(LuckyBun.config.devServer.listenHost).toBe('0.0.0.0') + expect(LuckyBun.config.devServer.host).toBe('127.0.0.1') + }) + test('user can override plugins', () => { createFile( 'config/bun.json', diff --git a/src/bun/lucky.js b/src/bun/lucky.js index 0429f922d..3b48b9d4a 100644 --- a/src/bun/lucky.js +++ b/src/bun/lucky.js @@ -271,12 +271,13 @@ export default { await this.build() await this.watch() - const {host, port, secure} = this.config.devServer + const {host, listenHost, port, secure} = this.config.devServer + const hostname = listenHost || (secure ? '0.0.0.0' : host) const debug = this.debug const wsClients = this.wsClients Bun.serve({ - hostname: secure ? '0.0.0.0' : host, + hostname, port, fetch(req, server) { if (server.upgrade(req)) return From c92fc2cc2e4bdb59d794fe2efb06eea87730690c Mon Sep 17 00:00:00 2001 From: Wout Date: Sat, 4 Apr 2026 12:14:40 +0200 Subject: [PATCH 7/9] Add `watchDirs` option to Bun config to limit watched files --- src/bun/lucky.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/bun/lucky.js b/src/bun/lucky.js index 3b48b9d4a..6ee7708da 100644 --- a/src/bun/lucky.js +++ b/src/bun/lucky.js @@ -45,6 +45,7 @@ export default { const defaults = { entryPoints: {js: ['src/js/app.js'], css: ['src/css/app.css']}, plugins: {css: ['aliases', 'cssGlobs'], js: ['aliases', 'jsGlobs']}, + watchDirs: ['src/js', 'src/css', 'src/images', 'src/fonts'], staticDirs: ['src/images', 'src/fonts'], outDir: 'public/assets', publicPath: '/assets', @@ -222,9 +223,7 @@ export default { }, async watch() { - const srcDir = join(this.root, 'src') - - watch(srcDir, {recursive: true}, (event, filename) => { + const handler = (event, filename) => { if (!filename) return let normalizedFilename = filename.replace(/\\/g, '/') @@ -262,7 +261,10 @@ export default { if (err.errors) for (const e of err.errors) console.error(e) } })() - }) + } + + for (const dir of this.config.watchDirs) + watch(join(this.root, dir), {recursive: true}, handler) console.log('Beginning to watch your project') }, From 764d35fc5eb18db66d5acf26acd4f0ed8f2b1f9f Mon Sep 17 00:00:00 2001 From: Wout Date: Sat, 4 Apr 2026 12:33:40 +0200 Subject: [PATCH 8/9] Add tests for `watchDirs` config --- spec/bun/lucky.test.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spec/bun/lucky.test.js b/spec/bun/lucky.test.js index 772128eb0..1429ffc3b 100644 --- a/spec/bun/lucky.test.js +++ b/spec/bun/lucky.test.js @@ -91,6 +91,7 @@ describe('loadConfig', () => { test('uses defaults without a config file', () => { LuckyBun.loadConfig() expect(LuckyBun.config.outDir).toBe('public/assets') + expect(LuckyBun.config.watchDirs).toEqual(['src/js', 'src/css', 'src/images', 'src/fonts']) expect(LuckyBun.config.entryPoints.js).toEqual(['src/js/app.js']) expect(LuckyBun.config.devServer.port).toBe(3002) expect(LuckyBun.config.plugins).toEqual({ @@ -113,6 +114,17 @@ describe('loadConfig', () => { expect(LuckyBun.config.entryPoints.js).toEqual(['src/js/app.js']) }) + test('merges watchDirs from user config', () => { + createFile( + 'config/bun.json', + JSON.stringify({watchDirs: ['src/js', 'src/css']}) + ) + + LuckyBun.loadConfig() + + expect(LuckyBun.config.watchDirs).toEqual(['src/js', 'src/css']) + }) + test('merges listenHost into devServer config', () => { createFile( 'config/bun.json', From b14f8676c8d70b214ba42329aa1e32b3678c7566 Mon Sep 17 00:00:00 2001 From: Wout Date: Sat, 4 Apr 2026 13:15:03 +0200 Subject: [PATCH 9/9] Check existence of watch dirs and warn if missing --- src/bun/lucky.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/bun/lucky.js b/src/bun/lucky.js index 6ee7708da..cb1afd0eb 100644 --- a/src/bun/lucky.js +++ b/src/bun/lucky.js @@ -263,8 +263,14 @@ export default { })() } - for (const dir of this.config.watchDirs) - watch(join(this.root, dir), {recursive: true}, handler) + for (const dir of this.config.watchDirs) { + const fullDir = join(this.root, dir) + if (!existsSync(fullDir)) { + console.warn(` ▸ Watch directory ${dir} does not exist, skipping...`) + continue + } + watch(fullDir, {recursive: true}, handler) + } console.log('Beginning to watch your project') },