tooling.js makes it easier to use nodejs for automation tasks.
Inspired by Jenkins Pipelines (specifically, the groovy based Jenkinsfile), tooling.js aims to make JavaScript friendlier for writing tooling for software projects.
Bash is clearly the defacto interpreter for the tasks required for building and testing software projects. However, as projects grow in size, bash can start to get unwieldy: error handling is complex, everything is in global scope, and parallelism requires arcane syntax and dropping state into tmp files.
JavaScript (especially with async/await), on the other hand, makes parallism and error handling loads better than bash. try/catch makes error handling reasonable straightforward (certainly easier than spending ten minutes on stack overflow to figure out if you should use == or -eq).
Of course, to take advantage of some of the most convenient advantes of JavaScript, we need to introduce babel and its requisite plugins - which is fine, but not work that we should repeat in every project we write. Enter tooling.js.
Tooling.js accepts a script file as an argument and passes it through two compilation stages. The second stage simply uses babel-present-env to ensure that all syntax is compatible with your local node version. The first stage introduces the globals described below.
npm install -g @ianwremmel/tooling.jsor
npm install --save-dev @ianwremmel/tooling.jswith the save-dev option, you'll want to define your executables with npm scripts.
Invoke tooling.js with
tooling automation.jsor
cat automation.js | toolingIn addition to injecting the globals described below, tooling.js wraps your script in an async IIFE, thus allowing you to use the await keyword at the top level of your script.
Note: Due to the semantics of the
importandexport, scripts that use theexportkeyword will not be wrapped in an async IIFE and allimportstatements must be at the top of the script.
parallel(
sh(`grunt test:unit`),
sh(`grunt test:node`),
sh(`grunt test:automation`)
)try {
sh(`exit 1`)
}
catch (err) {
if (err.code === 1) {
echo(`yowzers`);
}
else {
echo(`this should never be reached`);
}
}const result = sh(`exit $RANDOM`, {complex: true});
if (result.code === 1) {
echo(`exit with one`)
}
else {
echo(`did not exit with one`)
}Tooling.js provides a require hook at @ianwremmel/tooling.js/register. The following should work:
node -r @ianwremmel/tooling.js/register automation.jsor
require(`@ianwremmel/tooling.js/register`);
require(`./automation.js`);const transform = require(`@ianwremmel/tooling.js`);
eval(transform(require(`./automation.js`)));All async functions from fs are promisified and automatically prefixed with await. mkdir is replaced by mkirp.
const file = readFile(`in.txt`)
readFiledefaults to utf8 encoding
Changes the current directory
const os = require(`os`);
cd(os.tmpdir());Shorthand for console.log.
echo(`1`)Shorthand for process.env.
env.TEST_VAR = 5;Run multiple items in parallel. Note: every argument is wrapped in a promise, so arguments can be anything that can be passed to a function.
- concurrency: Number - maximum number of concurrent tasks to execute
parallel(
console.log(1),
new Promise((resolve) => {
process.nextTick(() => {
console.log(2);
resolve();
})
}),
console.log(3)
);parallel({concurrency: 2},
console.log(1),
new Promise((resolve) => {
process.nextTick(() => {
console.log(2);
resolve();
})
}),
console.log(3)
);prints the current directory when not assigned or returns it when assigned.
pwd()const dir = pwd()Reads a file and parses its contents as JSON. Will throw if the file does not contain valid JSON.
const json = readJSON(`in.json`);Execute an expression multiple times.
- repeat: Boolean - if true, the expression will be executed max times, even if it succeeds. default: false
- max: Number - maximum number of iterations. default: 3
retry(
new Promise((resolve, reject) => {
reject(new Error(`this will fail 3 times`));
})
)Note: rejected Promises must be rejected with
Errorobjects. This seems to have something to do with babel's async/await support.
retry({max: 2, repeat: true}
new Promise((resolve) => {
console.log(`this will print twice`);
})
)The variables ITERATION and MAX_ITERATIONS are injected into the running expression.
retry({max: 2, repeat: true}
new Promise((resolve) => {
console.log(`{ITERATION} out of ${MAX_ITERATIONS}`);
})
)Note:
ITERATIONis zero-based, so will never equalMAX_ITERATIONS.
Execute a shell command "synchronously" (actually wraps child_process.spawn in a promise and drops an await in front of it).
- complex: Boolean - if true, return the full object returned by spawn instead of just stdout
- spawn: Object - an object of options to pass directly to spawn.
sh(`echo 1`)try {
sh(`exit 5`);
}
catch(err) {
require(`assert`).equal(err.code, 5);
}const one = sh(`echo 1`, {complex: true}).stdout;Send stderr/stdout to additional locations.
tee({file: `out.log`, stderr: true, stdout: true});
echo(1);tee({file: `out.log`, stderr: true, stdout: true});
tee.silent = true;
echo(1);Note: Though it doesn't matter when you sent
tee.silent, doing so won't have any impact until after tee is called for the first time.
echo(1);
const t = tee({file: `out.log`, stderr: true, stdout: true})
echo(2);
t.stop();
echo(3);PRs accepted. Please lint and test your code with npm test