UNPKG

bimba-cli

Version:

The CLI tool to run Imba projects under Bun

271 lines (229 loc) 8.46 kB
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(''); }