bimba-cli
Version:
The CLI tool to run Imba projects under Bun
271 lines (229 loc) • 8.46 kB
JavaScript
import { plugin } from "bun";
import {theme} from './utils.js';
import * as compiler from 'imba/compiler'
import dir from 'path'
import fs from 'fs'
import { Glob } from "bun";
import { unlink } from "node:fs/promises";
export const cache = dir.join(process.cwd(), '.cache')
if (!fs.existsSync(cache)){ fs.mkdirSync(cache);}
// this should be reset from outside to get results of entrypoint building
export let stats = {
failed: 0,
compiled: 0,
cached: 0,
bundled: 0,
errors: 0,
reported: 0,
};
const _activeCompileErrors = new Map();
function normalizeCompilePath(filepath) {
let value = String(filepath || '').split(/[?#]/)[0];
if (dir.isAbsolute(value)) {
const rel = dir.relative(process.cwd(), value);
if (!rel.startsWith('..')) value = rel;
}
return value.replaceAll('\\', '/');
}
function physicalCompileKey(filepath) {
try {
const stat = fs.statSync(filepath);
if (!stat.isFile()) return null;
const real = fs.realpathSync(filepath).replaceAll('\\', '/');
return `fs:${stat.dev}:${stat.ino}:${real}`;
} catch(_) {
return null;
}
}
function compileErrorKey(filepath) {
return physicalCompileKey(filepath) || `path:${normalizeCompilePath(filepath)}`;
}
function compileFileStamp(filepath) {
try {
const stat = fs.statSync(filepath);
if (!stat.isFile()) return null;
return `${stat.mtimeMs ?? stat.mtime?.getTime?.() ?? 0}_${stat.size ?? 0}`;
} catch(_) {
return null;
}
}
function compileErrorMessage(error) {
return error?.message || String(error);
}
function compileErrorLine(error) {
return error?.range?.start?.line ?? error?.line ?? '';
}
function compileErrorSnippet(error) {
try {
return error?.toSnippet?.() || error?.snippet || error?.stack || compileErrorMessage(error);
} catch(_) {
return error?.snippet || error?.stack || compileErrorMessage(error);
}
}
function compileErrorSignature(errors) {
return errors
.map(error => [compileErrorMessage(error), compileErrorLine(error)].join('\n'))
.join('\n---\n');
}
function shouldPrintCompileError(filepath, errors) {
const key = compileErrorKey(filepath);
const signature = compileErrorSignature(errors);
const previous = _activeCompileErrors.get(key);
_activeCompileErrors.set(key, { file: normalizeCompilePath(filepath), signature, time: Date.now() });
return previous?.signature !== signature;
}
function clearCompileError(filepath) {
_activeCompileErrors.delete(compileErrorKey(filepath));
}
function printCompileError(error) {
try {
printerr(error);
} catch(_) {
console.log('');
console.log(' ' + theme.error(' ' + compileErrorMessage(error) + ' '));
console.log('');
}
}
// Target platform for the Imba compiler: 'browser' or 'node'
// Set via setTarget() from the CLI before building
export let target = 'browser';
export function setTarget(t) { target = t; }
export const imbaPlugin = {
name: "imba",
async setup(build) {
// when an .imba file is imported...
build.onLoad({ filter: /\.imba$/ }, async ({ path }) => {
const f = dir.parse(path)
while (true) {
const stamp = compileFileStamp(path);
if (!stamp) {
clearCompileError(path);
return { contents: '', loader: "js" };
}
let contents = '';
// return the cached version if exists (include target in hash to avoid cross-platform cache hits)
const cached = dir.join(cache, Bun.hash(path + ':' + target) + '_' + stamp + '.js');
if (fs.existsSync(cached)) {
const cachedContents = await Bun.file(cached).text();
if (compileFileStamp(path) !== stamp) continue;
clearCompileError(path);
stats.bundled++;
stats.cached++;
return {
contents: cachedContents,
loader: "js",
};
}
// clear previous cached version
const glob = new Glob(Bun.hash(path + ':' + target) + '_' + "*.js");
for await (const file of glob.scan(cache)) if (fs.existsSync(dir.join(cache, file))) unlink(dir.join(cache, file));
// if no cached version read and compile it with the imba compiler
const file = await Bun.file(path).text();
if (compileFileStamp(path) !== stamp) continue;
const platform = target === 'node' || target === 'bun' ? 'node' : 'browser';
let out
try {
out = compiler.compile(file, {
sourcePath: path,
platform: platform,
comments: false
})
} catch (error) {
out = { js: '', errors: [error] }
}
// Never report or cache a result from an older source snapshot.
if (compileFileStamp(path) !== stamp) continue;
// the file has been successfully compiled
if (!out.errors?.length) {
clearCompileError(path);
console.log(theme.action("compiling: ") + theme.folder(dir.join(f.dir,'/')) + theme.filename(f.base) + " - " + theme.success("compiled"));
stats.bundled++;
stats.compiled++;
contents = out.js;
await Bun.write(cached, contents);
}
// there were errors during compilation
else {
const shouldPrint = shouldPrintCompileError(path, out.errors);
if (shouldPrint) console.log(theme.action("compiling: ") + theme.folder(dir.join(f.dir,'/')) + theme.filename(f.base) + " - " + theme.failure(" fail "));
stats.failed++;
if (shouldPrint) {
stats.reported++;
for (let i = 0; i < out.errors.length; i++) {
if(out.errors[i]) printCompileError(out.errors[i]);
}
}
stats.errors++;
}
// and return the compiled source code as "js"
return {
contents,
loader: "js",
};
}
});
}
};
plugin(imbaPlugin);
// -------------------------------------------------------------------------------
// print pretty messages produced by the imba compiler
// -------------------------------------------------------------------------------
// print an error generated by the imba compiler
export function printerr(err) {
// halper function to produce empty strings
const fill = (len = 0) => {return new Array(len + 1).join(' ')}
// gather the needed information from the compiler error
const snippet = err.toSnippet().split("\n");
const errs = snippet[2] ? snippet[2].indexOf('^') : -1;
// no source context available — print compact fallback
if (!snippet[1] || errs === -1) {
console.log('');
console.log(fill(10) + theme.error(" " + err.message + " "));
console.log('');
return;
}
const display = {
error: " " + err.message + " ",
outdent: fill(10),
source: snippet[1] + " ",
margin: " line " + (err.range.start.line + 1) + " ",
errs: errs,
erre: snippet[2].lastIndexOf('^') + 1,
};
// calculate parameters for priniting a message
const center = display.margin.length + display.errs + Math.floor((display.erre - display.errs) / 2);
const half = Math.ceil((display.error.length - 1) / 2);
const start = Math.max(0, center - half);
const end = start + display.error.length;
const total = Math.max(display.margin.length + display.source.length, end);
// print emtpy line
console.log('');
// print line with the error message
console.log(
display.outdent +
theme.margin(fill(Math.min(start, display.margin.length))) +
theme.code(fill(Math.max(0, start - display.margin.length))) +
theme.error(display.error) +
theme.margin(fill(Math.max(0, display.margin.length - end))) +
theme.code(fill(Math.min(total - display.margin.length, total - end)))
);
// print line with the source code
console.log(
display.outdent +
theme.margin(display.margin) +
theme.code(display.source.slice(0,display.errs)) +
theme.error(display.source.slice(display.errs,display.erre)) +
theme.code(display.source.slice(display.erre)) +
theme.code(fill(total - display.source.length - display.margin.length))
);
// print empty line to balance the view
// later we can put something usefull here
// for example a link to online docs about the error
console.log(
display.outdent +
theme.margin(fill(display.margin.length)) +
theme.code(fill(total - display.margin.length))
);
// print emtpy line
console.log('');
}