UNPKG

bimba-cli

Version:

The CLI tool to run Imba projects under Bun

245 lines (217 loc) 9.73 kB
#!/usr/bin/env bun import { parseArgs } from "util"; import { imbaPlugin, stats, cache, setTarget } from './plugin.js' import { IMBA_RUNTIME_DEFINES, theme } from './utils.js'; import fs from 'fs' import path from 'path'; import { rmSync } from "node:fs"; import { serve } from './serve.js'; import { checkImbaTypes } from './typecheck.js'; let flags = {} let entrypoint = '' try { const { values, positionals } = parseArgs({ args: Bun.argv.slice(2), options: { watch: { type: 'boolean' }, outdir: { type: 'string' }, help: { type: 'boolean' }, clearcache: { type: 'boolean' }, minify: { type: 'boolean' }, splitting: { type: 'boolean' }, target: { type: 'string' }, external: { type: 'string', multiple: true }, sourcemap: { type: 'string' }, serve: { type: 'boolean' }, port: { type: 'string' }, html: { type: 'string' }, typecheck: { type: 'boolean' }, tscheck: { type: 'boolean' }, }, allowNegative: true, strict: true, allowPositionals: true, }); flags = values; entrypoint = positionals[0] || ''; } catch (error) { if (error instanceof Error) console.log(error.message); else console.log("Could not resolve CLI arguments. Read help to know them: " + theme.flags('--entry file.imba')); process.exit(0); } function ensureBunfigPreload() { const bunfigPath = path.join(process.cwd(), 'bunfig.toml'); const preloadLine = 'preload = ["bimba-cli/plugin.js"]'; if (!fs.existsSync(bunfigPath)) { fs.writeFileSync(bunfigPath, preloadLine + '\n'); console.log(theme.action("note: ") + theme.filename("bunfig.toml was not found, so bimba created it.")); console.log(theme.action(" ") + `Added ${theme.flags(preloadLine)} to preload the Imba plugin.`); return; } const content = fs.readFileSync(bunfigPath, 'utf8'); if (content.includes(preloadLine)) return; console.log(theme.action("note: ") + theme.filename("bunfig.toml already exists, so bimba did not edit it automatically.")); console.log(theme.action(" ") + `Add ${theme.flags(preloadLine)} manually if you want Bun to preload the plugin.`); } // help: more on bun building params here: https://bun.sh/docs/bundler if(flags.help) { console.log(""); console.log("Bimba requeres an .imba file and a folder where to put compiled .js files."); console.log("For example like this: "+theme.filedir('bimba file.imba --outdir public')); console.log(""); console.log(" "+theme.flags('--outdir <folder>')+" Compile imba files to the specified folder"); console.log(" "+theme.flags('--no-minify')+" Disable minification for compiled .js files"); console.log(" "+theme.flags('--sourcemap <inline|external|none>')+" How should sourcemap files be included in the .js"); console.log(" "+theme.flags('--target <browser|node>')+" Target platform for both Imba compiler and Bun bundler"); console.log(" "+theme.flags('--external <package>')+" Exclude package from bundle (repeatable, e.g. --external ws --external node-pty)"); console.log(" "+theme.flags('--watch')+" Watch for changes in the entrypoint folder"); console.log(" "+theme.flags('--clearcache')+" Clear cache on exit, works only when in watch mode"); console.log(" "+theme.flags('--typecheck')+" Check TypeScript diagnostics in .imba files"); console.log(" "+theme.flags('--tscheck')+" Alias for --typecheck"); console.log(""); console.log("Dev server (HMR):"); console.log(" "+theme.flags('--serve')+" Start dev server with Hot Module Replacement"); console.log(" "+theme.flags('--port <number>')+" Port for the dev server (default: 5200)"); console.log(" "+theme.flags('--html <path>')+" Custom HTML file path (auto-detected if omitted)"); console.log(""); process.exit(0); } let bundling = false; let rebuildQueued = false; let watchTimer = null; // typecheck mode if (flags.typecheck || flags.tscheck) { try { const success = await checkImbaTypes(entrypoint); process.exit(success ? 0 : 1); } catch (error) { console.log(theme.failure(' Failure ') + ` ${error.message}`); process.exit(1); } } // serve mode else if (flags.serve) { if (!entrypoint) { console.log(""); console.log("You should provide entrypoint: "+theme.flags('bimba file.imba --serve')); console.log(""); process.exit(1); } ensureBunfigPreload(); serve(entrypoint, { port: parseInt(flags.port) || 5200, html: flags.html }); } // no entrypoint or outdir else if(!entrypoint || !flags.outdir) { console.log(""); console.log("You should provide entrypoint and the output dir: "+theme.flags('bimba file.imba --outdir public')); console.log("For more information: "+theme.flags('--help')); console.log(""); process.exit(1); } // build else { ensureBunfigPreload(); const success = await bundle(); if (!success && !flags.watch) process.exit(1); watch(bundle); } function watch(callback) { if (flags.watch) { const watcher = fs.watch(path.dirname(entrypoint), {recursive: true}, () => { if (watchTimer) clearTimeout(watchTimer); watchTimer = setTimeout(() => { watchTimer = null; callback(); }, 150); }); process.on("SIGINT", () => { if(flags.clearcache) rmSync(cache, { recursive: true, force: true }); if(watcher) { watcher.close(); process.exit(0); } }); } } async function bundle() { if (bundling) { rebuildQueued = true; return false; } bundling = true; if (!fs.existsSync(entrypoint)) { console.log(theme.failure('Error.') + ` The specified entrypoint does not exist: ${theme.filedir(entrypoint)}`); return false; } stats.failed = 0 stats.compiled = 0 stats.errors = 0 stats.reported = 0 stats.bundled = 0 const start = Date.now(); console.log(theme.folder("──────────────────────────────────────────────────────────────────────")); console.log(theme.start(`Start building the Imba entrypoint: ${theme.filedir(entrypoint)}`)); // set Imba compiler platform based on target const buildTarget = flags.target || 'browser'; setTarget(buildTarget); let result = undefined try { const buildOpts = { entrypoints: [entrypoint], outdir: flags.outdir, target: buildTarget, sourcemap: flags.sourcemap || 'none', minify: flags.minify ?? true, splitting: flags.splitting || false, define: IMBA_RUNTIME_DEFINES, plugins: [imbaPlugin] }; if (flags.external?.length) { buildOpts.external = flags.external; } result = await Bun.build(buildOpts); // For node target, add shebang to the output file if (result.success && (buildTarget === 'node' || buildTarget === 'bun')) { for (const output of result.outputs) { const outPath = output.path; const content = fs.readFileSync(outPath, 'utf8'); if (!content.startsWith('#!')) { const shebang = buildTarget === 'bun' ? '#!/usr/bin/env bun' : '#!/usr/bin/env node'; fs.writeFileSync(outPath, shebang + '\n' + content); fs.chmodSync(outPath, 0o755); } } } if(stats.failed) { if (stats.reported) console.log(theme.start(theme.failure(" Failure ") + theme.filename(` Imba compiler failed to proceed ${stats.failed} file${stats.failed > 1 ? 's' : ''}`))); } else console.log(theme.start(theme.success("Success") +` It took ${theme.time(Date.now() - start)} ms to bundle ${theme.count(stats.bundled)} file${stats.bundled > 1 ? 's' : ''} to the folder: ${theme.filedir(flags.outdir)}`)); if (!result.success && !stats.errors) { for (const log of result.logs) { console.log(log); } } return result.success && !stats.failed && !stats.errors; } catch(error) { console.log(theme.folder("──────────────────────────────────────────────────────────────────────")); console.log('') console.log(error) console.log(theme.folder("──────────────────────────────────────────────────────────────────────")); console.log(theme.failure(" Failure ") + theme.filename(' Bun found an error in the compiled JS file')) return false; } finally { bundling = false; if (rebuildQueued) { rebuildQueued = false; queueMicrotask(bundle); } }; }