bunbun v3.0.3
bunbun
Node.JS based simple, lightweight task bunner... I mean runner
Why?
⏱ Lightweight and very fast - very simple in design, without any library compilers that you will never use, just plain task runner
📐 Ready to use - just add to your project, create file and prepare your first task
🔌 Extendable - "extends-by-code" philosophy - you are owner of your build flow, without any magic and invisible parts/configurations etc., neither no compilers included
✨ Universal - this library contains only language/tool/framework agnostic tools, all compilations/copying things are in your hands
🎈 Without magic - all of your tasks are just JS code, without any configs, 3rd-party plugins etc. just plain source code, therefore the learning curve is very low
🐞 Debuggable - debugging is very simple because none of all tasks are done invisibly underhood, it's easy to find erroring part if you see all steps of build process, also you can enable debugging mode in Bunbun to log all things
🧰 Safe versioning - version of Bunbun will never break any of building processes because Bunbun don't conains any API for them, updating Bunbun will need from you only update your building script - it's all, you will never have to wait for a some plugin to be updated
Requirements
Node.JS >= 7.6v
- ...because of default support of async/await syntax
- or
Node.JS >= 10v
- ...for execution and handling processes
NPM
How to install?
Simplest way is just using npm:
npm install --save-exact --save-dev bunbun
Real example
Example code:
const { build } = require('esbuild');
const nodeSass = require('node-sass');
const Bunbun = require('./../dist/main.js');
const sass = args => new Promise((res, rej) => {
nodeSass.render(args, (err, data) => {
err ? rej(err) : res(data);
});
});
const hash = async file => {
const res = await $.fs.hash(file, 'md5', 'base64');
return res.replace(/[^a-z0-9]/gi, '');
};
const SOURCE_EXTS = '(scss|ts|tsx|js|jsx|html)';
const $ = new Bunbun;
$.task('build:typescript', async () => {
await $.until('hash');
await build({
sourcemap: true,
format: 'iife',
minify: true,
bundle: true,
outfile: './build/index.js',
entryPoints: ['./src/index.tsx'],
platform: 'browser',
tsconfig: './tsconfig.json',
define: {
'process.env.NODE_ENV': '"develop"',
},
});
$.run('hash');
});
$.task('build:scss', async () => {
await $.until('hash');
const result = await sass({
file: './src/index.scss',
sourceMap: './index.css.map',
outFile: './index.css',
outputStyle: 'compressed',
sourceMapContents: true,
});
await $.fs.write('./build/index.css', result.css || '');
await $.fs.write('./build/index.css.map', result.map || '');
$.run('hash');
});
$.task('hash', async () => {
await $.fs.edit('./src/index.html', async html => {
const jsHash = await hash('./build/index.js');
const cssHash = await hash('./build/index.css');
return html
.replace('__JS_HASH__', jsHash)
.replace('__CSS_HASH__', cssHash);
});
});
$.alias('build', ['build:typescript', 'build:scss']);
$.task('assets', async () => {
let list = await $.fs.list(['./src/**/*.*', `!**/*.${SOURCE_EXTS}`]);
for (const x of list) {
await $.fs.copy(x, x.replace(/^\.\/src/, './build'));
}
});
$.task('watch', async () => {
const server = $.serve('./build', 8080);
$.fs.watch(`./src/**/*.${SOURCE_EXTS}`, async () => {
await $.run('build');
server.reload();
});
$.fs.watch(`./src/**/*.!${SOURCE_EXTS}`, async () => {
await $.run('assets');
server.reload();
});
});
$.start('build');
Table of Contents
class Bunbun
fs
- filesystem APIlogger
- logger APIalias()
- regiser alias of multiple tasksdebounce()
- allows to debounce by promise and timeexec()
- execute terminal commandhash()
- hash given filerescue()
- catch exception making result optionalrun()
- run registered taskstart()
- run tasks byprocess.argv
serve()
- create new http servertask()
- register new taskuntil()
- waits until task has been donewait()
-setTimeout
but inawait
/async
convention
class Logger
class Fs
cwd
- current working directorycopy()
- copy (also recursive and glob option) files/dirscreateDir()
- create dircreateTempDir()
- create unique temporary diredit()
- reads and writes back fileexists()
- check whenever file/dir exists and returns typehash()
- hash given filelist()
- list files/dirsread()
- reads data from fileremove()
- removes file/dirrename()
- renames (also moves) file/dirwatch()
- watches files/dirs for changeswrite()
- writes data into file
class HttpServer
reload()
- reloads all html pages
Main class of whole script, it's also default exported thing from module
type Options = {
// Determines if logger should be louder by default
debugging: boolean; // default: false
// Determines if logger should be silent by default
// this option has higher priority than `debugging`
silent: boolean; // default: false
// Determines if tasks should wait for previous promise by default
debouncePromise: boolean; // default: true
// Determines if tasks should debounce and with what time by default
// 0 means turn off
debounceTime: number; // default: 200
// Current working directory for fs/exec etc.
cwd: string; // default: process.cwd()
};
type Bunbun = {
new(options?: Partial<Options>);
// ...
};
const Bunbun = require('bunbun');
// Debugging instance
const $1 = new Bunbun({
debugging: true,
});
// Default instance
const $2 = new Bunbun();
Fs
(filesystem) instance available for external usage
fs: Fs;
const $ = new Bunbun;
$.fs.read('./file.txt').then(data => {
// ...
});
Logger
instance used by Bunbun and available for external usage as well
logger: Logger;
const $ = new Bunbun;
const myVar = { test: 1 };
$.logger.log('my var = $$', myVar);
Registers alias of tasks, those tasks will be executed asynchronously. Name of such alias will be in same pool where tasks are so you can't register task of same name which already exists
alias(
name: string,
tasks: string[],
): void;
const $ = new Bunbun;
$.task('foo', () => {
console.log('foo');
});
$.task('bar', async () => {
await someLongAction();
console.log('bar');
});
$.alias('baz', ['bar', 'foo']);
// is equal to
$.task('qux', async () => {
return Promise.all([
$.run('bar'),
$.run('foo'),
]);
});
$.run('baz'); // or $.run('qux');
// output:
// >> foo
// >> bar (because it's waited for long running function)
Creates debounce function of given function, this kind of function allows you avoid multiple calls of time-critical functions, e.g:
- To avoid overloop building process for same code
- To avoid overwriting things at overwriting they in previous call
💡 Tip - time debouncing don't call function until given timeout, promise debouncing call function and wait for fulfilling last promise
debounce<T>(
fn: () => Promise<T> | T,
time: number = 0,
): Promise<T>;
const $ = new Bunbun;
// Not working debounce (without promise nor time)
let i1 = 0;
const fnNothing = $.debounce(() => {
console.log('1', i1 += 1);
});
// Promise based debounce
let i2 = 0;
const fnPromise = $.debounce(() => {
new Promise(res => setTimeout(() => {
console.log('2', i2 += 1);
res();
}, 10 * 1000))
});
// Time based debounce
let i3 = 0;
const fnTime = $.debounce(() => {
console.log('3', i3 += 1);
}, 10 * 1000);
// Promise and time based debounce
let i4 = 0;
const fnPromiseAndTime = $.debounce(() =>
new Promise(res => setTimeout(() => {
console.log('4', i4 += 1);
res();
}, 9 * 1000))
, 9 * 1000);
// --- results ---
fnNothing(); // > 1
fnNothing(); // > 2
fnNothing(); // > 3
fnNothing(); // > 4
fnPromise(); // > wait for promise > 1
// after 5s
fnPromise(); // > wait for previous result > 1
// after 5s
fnPromise(); // > wait for new promise > 2
// after 10s
fnPromise(); // > wait for new promise > 3
fnTime(); // > wait for timeout > 1
// after 5s
fnTime(); // > wait for previous timeout > 1
// after 5s
fnTime(); // > wait for new timeout > 2
// after 10s
fnTime(); // > wait for new timeout > 3
fnPromiseAndTime(); // > wait for timeout > wait for promise > 1
// after 5s
fnPromiseAndTime(); // > wait for previous timeout > wait for previous promise > 1
// after 5s
fnPromiseAndTime(); // > wait for previous promise > 1
// after 10s
fnPromiseAndTime(); // > wait for new timeout > wait for new promise > 2
Allows you to execute command in shell and wait for result
💡 Tip - if timeout is equal or less than 0 then timeout will don't be turned on
exec(
command: string,
timeout: number = 0,
): Promise<{
stdout: string;
stderr: string;
}>;
const $ = new Bunbun;
$.exec('node -v').then(({ stdout }) => {
console.log(stdout);
});
// or
$.task('node-v', async () => {
const { stdout } = await $.exec('node -v');
console.log(stdout);
});
Hash given string
hash(
text: string,
algorithm: 'md5' | 'sha1' | 'sha256' | 'sha512' = 'md5',
encoding: 'hex' | 'base64' | 'buffer' | 'latin1' = 'base64'
): Promise<string>;
const $ = new Bunbun;
$.hash('test').then(hash => console.log(hash));
Allows catch promise and keep async
/await
syntax
💡 Tip - be careful and never call (
await
)rescue(await someFunc())
because thissomeFunc()
will throw in higher, current context instead ofrescue
's context, instead of that callawait rescue(someFunc())
🎈 Fun fact - in Ruby language
rescue
keyword is used instead of typicalcatch
keyword - that's why Bunbun use this name in this context
rescue<T1, T2>(
promise: Promise<T1> | T1,
alter?: T2,
): Promise<T1 | T2>;
const $ = new Bunbun;
const justReject = async () => {
throw 'wowie, such error';
};
const dontReject = async () => 'ok!';
$.task('test-1', async () => {
const error = await $.rescue(justReject());
console.log(error); // > 'wowie, such error'
});
$.task('test-2', async () => {
const error = await $.rescue(await justReject());
// unreachable code because `justReject` is executed
// in current context!
});
$.task('test-3', async () => {
const ok = await $.rescue(dontReject());
console.log(ok); // > 'ok!'
});
$.task('test-4', async () => {
const ok = await $.rescue('ok!');
console.log(ok); // > 'ok!'
});
$.task('test-5', async () => {
const alternativeValue = await $.rescue(justReject(), 'ok!');
console.log(alternativeValue); // > 'ok!'
});
Run single task by name and returns promise of call of this task
💡 Tip - if task with such name will be unable to found then this function will throw
false
run(name: string): Promise<boolean>;
const $ = new Bunbun;
$.task('reject', async () => {
throw 'oh no!';
});
$.task('resolve', async () => {
return 'ok!';
});
$.task('test', async () => {
// try run task
await $.rescue($.run('reject'));
console.log(
await $.rescue($.run('reject')
); // > 'oh no!'
console.log(
await $.rescue($.run('?')
); // > false
console.log(
await $.rescue($.run('resolve')
); // > true
});
Similar to bunbun.run()
but instead of running single task allows run multiple tasks, with single fallback task. This function will don't throw at error (only log it)
💡 Tip - this function is created mainly for using as main method
start(
defaultTask: string = 'default',
tasks: string[] = process.argv.slice(2),
): Promise<void>;
const $ = new Bunbun;
$.task('reject', async () => {
throw 'oh no!';
});
$.task('resolve', async () => {
return 'ok!';
});
$.start(); // run 'default' task or given names from terminal
// or
$.start('resolve'); // run 'resolve' task or given names from terminal
// or
$.start(
'resolve',
['resolve', 'reject'],
); // run always 'resolve' and 'reject'
Creates new instance of class HttpServer
type Options = {
fallback: string = './index.html',,
reload: boolean = true,
reloadPort: number = 8181,
};
serve(
directory: string,
port: number,
options?: Partial<Options>,
): HttpServer;
const $ = new Bunbun;
const server = $.serve('build', 8080);
// after some action you can force reload of pages
server.reload();
Register new task under given name. Task can be simple function or async function if needed
💡 Tip - if you cannot use
async
/await
syntax then returningPromise
instance will give same effect
task(
name: string,
fn: () => unknown,
): void;
const $ = new Bunbun;
$.task('foo', () => {
console.log('foo');
});
$.task('bar', async () => {
await someLongAction();
console.log('bar');
});
$.task('baz', async () => {
await $.run('foo');
await $.run('bar');
});
$.task('qux', async () => {
await $.run('bar');
await $.run('foo');,
});
$.run('baz');
// output:
// >> foo
// >> bar (because it's waited for long running function)
$.run('qux');
// output:
// >> bar
// >> foo (because it's waited until previous task ended)
Waits until given task has been completed, it doesn't matter if it ended successfully, throws if task has been not found
until(name: string): Promise<boolean>;
const $ = new Bunbun;
$.task('long', async () => {
await $.wait(1500);
});
$.task('run-and-wait', async () => {
$.run('long');
await $.until('long');
// waits until 'long' ended
await $.until('long');
// pass immediately because 'long' is not running
});
setTimeout()
alternative for async
/await
syntax
wait(time: number): Promise<void>;
const $ = new Bunbun;
$.task('long', async () => {
console.log('wait moment');
await $.wait(1500);
console.log('hello again');
});
Class prepared for logging purposes. Logger is already constructed with class Bunbun
with default settings
type Logger = {
new();
// ...
};
const $ = new Bunbun();
$.logger; // here you are
Determines if logger should log more useful data for debugging purposes
debugging: boolean = false;
const $ = new Bunbun();
$.logger.debugging = true;
// Now bunbun will throw more logs
$.logger.debugging = false;
// Now bunbun will don't throw so much data
Determines if logger should be silent
silent: boolean = false;
const $ = new Bunbun();
$.logger.silent = true;
// Now bunbun will be silent
$.logger.silent = false;
// Now bunbun will be loud
Using logger.format()
function to colorize data, adds prefix and log given thing. Nothing will be logged if logger.silent
is true. logger.debug()
will log things only if logger.debugging
is true
debug(template: string, ...args: any[]): void;
// or
error(template: string, ...args: any[]): void;
// or
log(template: string, ...args: any[]): void;
// or
success(template: string, ...args: any[]): void;
const $ = new Bunbun();
$.logger.debug('my var = $$', 10);
// logs only if 'logger.debugging' is equal to 'true'
// > '? ~ my var = 10'
$.logger.error('my var = $$', 10);
// > '✗ ~ my var = 10'
$.logger.log('my var = $$', 10);
// > ' ~ my var = 10'
$.logger.success('my var = $$', 10);
// > '✔ ~ my var = 10'
Format given template using built-in Node's functions, but uses $$
as placeholder for any type of variable
format(template: string, ...args: any[]): string;
const $ = new Bunbun();
console.log(
$.logger.format(
'1.$$\n2.$$\n3.$$',
10,
'test',
{ test: 10 },
)
);
// > output:
// 1.10
// 2.'test'
// 3.{ test: 10 }
Class prepared for filesystem manipulations or fetching data purposes. Fs is already constructed with class Bunbun
with default settings
type Fs = {
new();
// ...
};
const $ = new Bunbun();
$.fs; // here you are
Current working directory for all methods of class Fs
cwd: string = process.cwd();
const $ = new Bunbun();
$.fs.cwd === process.cwd(); // > true
$.fs.cwd = 'C:/src'; // now all methods starts from "C:/src"
Copy single file or whole dir (recursively), returns true
if done without any errors, errors will be throwed as exception
copy(
source: string,
target: string
): Promise<true>;
const $ = new Bunbun();
$.task('maybe-copy-file', async () => {
const res = await $.rescue(
$.fs.copy('file.txt', 'second-file.txt'),
false
);
if (res) {
// file has been copied
} else {
// file has NOT been copied
}
});
$.task('swallow-copy-file', async () => {
await $.rescue($.fs.copy('file.txt', 'second-file.txt'));
// nobody cares if file has been copied
});
$.task('force-copy-file', async () => {
await $.fs.copy('file.txt', 'second-file.txt');
// unreachable without successful copying file
});
If any directory in tree does not exists then they will be created - otherwise nothing will be created
createDir(
path: string,
mode?: number // e.g 0o776
): Promise<true>;
const $ = new Bunbun();
$.task('maybe-create-dir', async () => {
const res = await $.rescue(
$.fs.createDir('test/test/test'),
false
);
if (res) {
// dir is prepared
} else {
// dir isn't prepared for some reason
}
});
$.task('swallow-create-dir', async () => {
await $.rescue($.fs.createDir('test/test/test'));
// nobody cares if dir exists
});
$.task('force-create-dir', async () => {
await $.fs.createDir('file.txt', 'second-file.txt');
// unreachable without successful dir creating or check if exists
});
Creates dir in temporary directory of your operating system, those directories are often cleared
createTempDir(): Promise<string>;
const $ = new Bunbun();
$.task('temp-dir', async () => {
const dir = await $.fs.createTempDir();
// "dir" variable contains temporary directory path
});
Allows seamlessly edit content of file
edit(
path: string,
fn: (data: string) => (Promise<string> | string)
): Promise<void>;
const $ = new Bunbun();
$.task('edit-file', async () => {
await $.fs.edit(
'test.txt',
data => data.replace('$$', 'USD')
);
// file has been edited
});
Check if given path exists and if so, returns type of given path
exists(path: string): Promise<false | 'file' | 'dir'>;
const $ = new Bunbun();
$.task('check-path', async () => {
const type = await $.fs.exists('test');
switch (type) {
case false:
// does not exists
break;
case 'file':
// path is file
break;
case 'dir':
// path is directory
break;
}
});
Hash given file
hash(
file: string,
algorithm: 'md5' | 'sha1' | 'sha256' | 'sha512' = 'md5',
encoding: 'hex' | 'base64' | 'buffer' | 'latin1' = 'base64'
): Promise<string>;
const $ = new Bunbun;
$.fs.hash('test.txt').then(hash => console.log(hash));
Returns list of files matching given pattern
type ListOptions = {
absolute: boolean = false,
cwd: string = fs.cwd,
onlyDirectories: boolean = false,
onlyFiles: boolean = false,
};
exists(
pattern: string | string[],
options: Partial<ListOptions> = {}
): Promise<string[]>;
const $ = new Bunbun();
$.task('assets-files', async () => {
const images = await $.fs.list([
'./src/**/*.(png|jpg)',
'!./src/**/*.raw.png'
]);
// images is array of matching paths
});
Reads content of file as string
read(path: string): Promise<string>;
const $ = new Bunbun();
$.task('read-files', async () => {
const text = await $.fs.read('package.json');
// text is package.json content
});
Removes given file or dir
remove(path: string): Promise<true>;
const $ = new Bunbun();
$.task('remove-file', async () => {
const text = await $.fs.remove('package.json');
// package.json is removed here
});
Renames given file or dir
rename(source: string, target: string): Promise<true>;
const $ = new Bunbun();
$.task('rename-file', async () => {
await $.fs.rename('package.json', 'package.json.bak');
// package.json is renamed here
});
Watches given files which matches given paths and returns disposer, every time any matching file has been changed, removed or added then given function will be executed, also after initial scanning function is executed once
watch(
pattern: string | string[],
fn: () => any
): () => void;
const $ = new Bunbun();
$.fs.watch('./src/**/*.js', () => {
console.log('some file has been changed!');
});
Writes given content into file
watch(
path: string,
data: string | Buffer
): void;
const $ = new Bunbun();
$.fs.write('test.txt').then(() => {
console.log('DONE!');
});
Can be created via bunbun.serve()
type HttpServer = {
new();
// ...
};
const $ = new Bunbun;
const server = $.serve('build', 8080); // here you are
Reloads all pages with injected reloading script if turned on at creating
reload(): void;
const $ = new Bunbun;
const server = $.serve('build', 8080);
// after some change you can ask for reload pages:
server.reload();