bimba-cli
Version:
The CLI tool to run Imba projects under Bun
245 lines (217 loc) • 9.73 kB
JavaScript
#!/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);
}
};
}