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 diff --git a/spec/bun/lucky.test.js b/spec/bun/lucky.test.js index c10dc13bd..1429ffc3b 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) @@ -87,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({ @@ -109,6 +114,29 @@ 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', + 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', @@ -187,6 +215,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/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/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 diff --git a/src/bun/lucky.js b/src/bun/lucky.js index 33382eaba..cb1afd0eb 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 }, @@ -43,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', @@ -87,7 +90,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) { @@ -219,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, '/') @@ -259,7 +261,16 @@ export default { if (err.errors) for (const e of err.errors) console.error(e) } })() - }) + } + + 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') }, @@ -268,11 +279,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 @@ -281,11 +294,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() {} } 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