npm run enhanced - A powerful task runner and build tool for modern JavaScript projects.
- Compatible with
npm runfor npm scripts - Concurrent & Serial execution of tasks
- JavaScript extensibility with functions and promises
- Provider packages for reusable task libraries
- TypeScript support with automatic tsx or ts-node integration
- Advanced CLI with argument parsing and remaining args support
- Namespace organization for better task management
- and more
Running npm scripts
This module provides a command xrun to run all your npm scripts in package.json.
And you can run multiple of them concurrently or serially.
Some examples below:
| what you want to do | npm command | xrun command |
|---|---|---|
run test |
npm run test |
xrun test |
run lint and test concurrently |
N/A | xrun lint test |
run lint and then test serially |
N/A | xrun --serial lint test |
Alias for the options:
-s:--serial
You can write your tasks in JavaScript and run them with xrun.
This is useful when a shell script is too long to fit in a JSON string, or when it's not easy to do something with shell script.
These APIs are provided: concurrent, serial, exec, env, and load.
Put your tasks in a file xrun-tasks.js and xrun will load it automatically.
An example xrun-tasks.js:
const { load, exec, concurrent, serial } = require("@xarc/run");
load({
//
// define a task hello, with a string definition
// because a string is the task's direct value, it will be executed as a shell command.
//
hello: "echo hello",
//
// define a task world, using a JavaScript function to print something
//
world: () => console.log("world"),
//
// define a task serialTask, that will execute the three tasks serially, first two are
// the hello and world tasks defined above, and 3rd one is a shell command defined with exec.
// because the 3rd one is not a direct value of a task, it has to use exec to define a shell command.
//
serialTask: serial("hello", "world", exec("echo hi from exec")),
//
// define a task concurrentTask, that will execute the three tasks concurrently
//
concurrentTask: concurrent("hello", "world", exec("echo hi from exec")),
//
// define a task nesting, that does complex nesting of concurrent/serial constructs
//
nesting: concurrent(serial("hello", "world"), serial("serialTask", concurrent("hello", "world")))
});To run the tasks defined above from the command prompt, below are some examples:
| what you want to do | command |
|---|---|
run hello |
xrun hello |
run hello and world concurrently |
xrun hello world |
run hello and then world serially |
xrun --serial hello world |
Use exec to invoke a shell command from JavaScript.
Here are some examples:
| shell script in JSON string | shell script using exec in JavaScript |
note |
|---|---|---|
echo hello |
exec("echo hello") |
|
FOO=bar echo hello $FOO |
exec("FOO=bar echo hello $FOO") |
|
echo hello && echo world |
exec("echo hello && echo world") |
|
echo hello && echo world |
serial(exec("echo hello"), exec("echo world")) |
using serial instead of && |
execsupportsoptionsthat can set a few things. Some examples below:
| what you want to do | shell script using exec in JavaScript |
|---|---|
| setting an env variable | exec("echo hello $FOO", {env: {FOO: "bar"}}) |
| provide tty to the shell process | exec("echo hello", {flags: "tty"}) |
| using spawn with tty, and setting env | exec("echo hello $FOO", {flags: "tty,spawn", env: {FOO: "bar"}}) |
A task in JavaScript can be just a function.
load({
hello: () => console.log("hello")
});A function task can do a few things:
- Return a promise or be an async function, and
xrunwill wait for the Promise. - Return a stream and
xrunwill wait for the stream to end. - Return another task for
xrunto execute further. - Access parsed options with
context.argOpts.
Example:
load({
// A function task named hello that access parsed options with `context.argOpts`
async hello(context) {
console.log("hello argOpts:", context.argOpts);
return ["foo"];
},
h2: ["hello world"],
foo: "echo bar"
});Use concurrent and serial to define a task that run multiple other tasks concurrently or serially.
Some examples:
- To do the same thing as the shell script
echo hello && echo world:
serial(exec("echo hello"), exec("echo world"));- or concurrently:
concurrent(exec("echo hello"), exec("echo world"));- You can specify any valid tasks:
serial(
exec("echo hello"),
() => console.log("world"),
"name-of-a-task",
concurrent("task1", "task2")
);env allows you to create a task to set variables in process.env.
You use it by passing an object of env vars, like env({VAR_NAME: "var-value"})
Examples:
load({
setEnv: serial(env({ FOO: "bar" }), () => console.log(process.env.FOO))
});A popular CI/CD use case is to start servers and then run tests, which can be achieved using xrun JavaScript tasks:
const { concurrent, serial, load, stop } = require("@xarc/run");
const waitOn = require("wait-on");
const waitUrl = url => waitOn({ resources: [url] });
load({
"start-server-and-test": concurrent(
// start the servers concurrently
concurrent("start-mock-server", "start-app-server"),
serial(
// wait for servers concurrently, and then run tests
concurrent("wait-mock-server", "wait-app-server"),
"run-tests",
// Finally stop servers and exit.
// This is only needed because there are long running servers.
() => stop()
)
),
"start-mock-server": "mock-server",
"start-app-server": "node lib/server",
"wait-mock-server": () => waitUrl("http://localhost:8000"),
"wait-app-server": () => waitUrl("http://localhost:3000"),
"run-tests": "cypress run --headless -b chrome"
});
xrunaddsnode_modules/.binto PATH. That's whynpxis not needed to run commands likecypressthat's installed innode_modules.
@xarc/run supports provider packages - reusable task libraries that can be shared across projects. This allows teams to standardize common build tasks and workflows.
A provider package is identified by either:
- Having
xrunProviderconfig in itspackage.json - Having
@xarc/runas a dependency
// In your provider package's package.json
{
"name": "my-build-tasks",
"xrunProvider": {
"module": "tasks.js" // optional: specify which module exports loadTasks
}
}// In your provider's tasks.js (or main module)
module.exports = {
loadTasks(xrun) {
// can pass in optional namespace with xrun.load("namespace", {...})
return xrun.load({
build: "webpack --mode=production",
test: "jest",
lint: "eslint src/",
ci: ["lint", "test", "build"]
});
}
};Provider packages are automatically loaded when:
- You have no tasks loaded (automatic discovery)
- You explicitly enable them by setting
loadProviderModules: truein your@xarc/runconfig
Provider tasks are loaded from:
dependenciesdevDependenciesoptionalDependencies
Example package.json:
{
"name": "my-app",
"dependencies": {
"my-build-tasks": "^1.0.0"
},
"@xarc/run": {
"loadProviderModules": true
}
}Now you can run provider tasks directly:
xrun build # runs the build task from my-build-tasks
xrun ci # runs the ci task which executes lint, test, build seriallyNot a fan of full API names like concurrent, serial, exec? You can skip them.
concurrent: Any array of tasks are concurrent, except when they are specified at the top level.exec: Any string starting with~$are treated as shell script.serial: An array of tasks specified at the top level is executed serially.
Example:
load({
executeSerially: ["task1", "task2"], // top level array serially
concurrentArray: [["task1", "task2"]], // Any other array (the one within) are concurrent
topLevelShell: "echo hello", // top level string is a shell script
shellScripts: [
"~$echo hello", // any string started with ~$ is shell script
"~(tty,spawn)$echo hello" // also possible to specify tty and spawn flag between ~ and $
]
});-
Core Execution Engine
- Serial and concurrent task execution with proper nesting hierarchy
- Promise, node.js stream, or callback support for JavaScript tasks
- Run time flow control - return further tasks to execute from JS task functions
- Tasks can have a finally hook that always runs after task finish or fail
-
Developer Experience
- Compatible with and loads npm scripts from
package.json - Auto completion for bash and zsh
- TypeScript support with automatic tsx/ts-node loading (tsx preferred)
- Advanced CLI with comprehensive options (see CLI reference)
- Argument parsing with
--remaining args support - Specify complex task execution patterns from command line
- Compatible with and loads npm scripts from
-
Extensibility & Organization
- Provider packages - reusable task libraries for sharing common workflows
- Namespaces for organizing tasks across modules
- Define tasks in JavaScript files with full programmatic control
- Support flexible function tasks that can return more tasks to run
- Custom task execution reporters
-
Advanced Features
- TTY control for interactive commands
- Environment variable management with
env()tasks - Shell command execution with
exec()and spawn options - Task dependency resolution and execution planning
Still reading? Maybe you want to take it for a test drive?
Here is a simple sample.
- First setup the directory and project:
mkdir xrun-test
cd xrun-test
npm init --yes
npm install rimraf @xarc/run- Save the following code to
xrun-tasks.js:
"use strict";
const { load } = require("@xarc/run");
const tasks = {
hello: "echo hello world", // a shell command to be exec'ed
jsFunc() {
console.log("JS hello world");
},
both: ["hello", "jsFunc"] // execute the two tasks serially
};
// Load the tasks into @xarc/run
load(tasks);- And try one of these commands:
| what to do | command |
|---|---|
run the task hello |
xrun hello |
run the task jsFunc |
xrun jsFunc |
run the task both |
xrun both |
run hello and jsFunc concurrently |
xrun hello jsFunc |
run hello and jsFunc serially |
xrun --serial hello jsFunc |
Here is a more complex example to showcase a few more features:
"use strict";
const util = require("util");
const { exec, concurrent, serial, env, load } = require("@xarc/run");
const rimraf = util.promisify(require("rimraf"));
const tasks = {
hello: "echo hello world",
jsFunc() {
console.log("JS hello world");
},
both: {
desc: "invoke tasks hello and jsFunc in serial order",
// only array at top level like this is default to serial, other times
// they are default to concurrent, or they can be marked explicitly
// with the serial and concurrent APIs (below).
task: ["hello", "jsFunc"]
},
// invoke tasks hello and jsFunc concurrently as a simple concurrent array
both2: concurrent("hello", "jsFunc"),
shell: {
desc: "Run a shell command with TTY control and set an env",
task: exec({ cmd: "echo test", flags: "tty", env: { foo: "bar" } })
},
babel: exec("babel src -D lib"),
// serial array of two tasks, first one to set env, second to invoke the babel task.
compile: serial(env({ BABEL_ENV: "production" }), "babel"),
// more complex nesting serial/concurrent tasks.
build: {
desc: "Run production build",
task: serial(
() => rimraf("dist"), // cleanup, (returning a promise will be awaited)
env({ NODE_ENV: "production" }), // set env
concurrent("babel", exec("webpack")) // invoke babel task and run webpack concurrently
)
}
};
load(tasks);If you'd like to get the command xrun globally, you can install this module globally.
$ npm install -g @xarc/runHowever, it will still try to require and use the copy from your node_modules if you installed it.
If you don't want to use the CLI, you can load and invoke tasks in your JavaScript code using the run API.
Example:
const { run, load, concurrent } = require("@xarc/run");
const myTasks = require("./tools/tasks");
load(myTasks);
// assume task1 and task2 are defined, below will run them concurrently
run(concurrent("task1", "task2"), err => {
if (err) {
console.log("run tasks failed", err);
} else {
console.log("tasks completed");
}
});Promise version of
runisasyncRun
Name your task file xrun-tasks.ts if you want to use TypeScript.
You need to install a TypeScript runtime to your node_modules. @xarc/run supports both tsx (recommended) and ts-node:
# Recommended: tsx (faster, better ESM support)
npm install -D tsx typescript
# Alternative: ts-node
npm install -D ts-node typescriptxrun automatically detects and loads the appropriate TypeScript runtime when it finds xrun-tasks.ts, xrun-tasks.tsx, or xrun-tasks.mts files. It tries tsx first, then falls back to ts-node/register.
Any task can be invoked with the command xrun:
$ xrun task1 [task1 options] [<task2> ... <taskN>]ie:
$ xrun buildYou can pass arguments after -- to shell commands. These arguments are automatically appended to the last shell task:
$ xrun build -- --watch --verbose
$ xrun test -- --grep "specific test"For JavaScript function tasks, parsed options are available via the context param:
It's also pass as the this context for the function.
load({
myTask(context) {
console.log("Parsed options:", context.argOpts);
}
});Common CLI options include:
--serial,-s- Execute tasks serially instead of concurrently--cwd <path>,-w- Set working directory--list,-l- List available tasks--npm,-n- Load npm scripts (default: true)--quiet,-q- Suppress output--soe <mode>,-e- Stop on error mode:no,soft,full
For complete CLI reference:
$ xrun -hSee CLI Options for full details.
To load npm scripts into the npm namespace, use the --npm option:
This is enabled by default. To turn it off use --no-npm option.
$ xrun --npm testYou can also specify command line options under @xarc/run in your package.json.
- You can specify your tasks as an array from the command line. For example, to have
xrunexecute the tasks[task_a, task_b]concurrently:
$ xrun [task_a, task_b]
$ xrun --concurrent [task_a, task_b]- You can also execute them serially with:
$ xrun [--serial, task_a, task_b]
$ xrun --serial [task_a, task_b]- You can execute tasks serially, and then an inner array with concurrent tasks. The following will execute
task_a, thentask_b, and finallytask_c1andtask_c2concurrently
$ xrun --serial [task_a, task_b, [task_c1, task_c2]]- You can also make inner arrays serial using
--serialas the first element. Other shortcuts for "--serial" are:.and-s.
$ xrun [task_a, task_b, [--serial, task_c1, task_c2]]- You can pass the whole array in as a single string, which will be parsed as an array with string elements only.
$ xrun "[task_a, task_b, [task_c1, task_c2]]"Task name is any alphanumeric string that does not contain /, or starts with ? or ~$.
Tasks can be invoked from command line:
xrun foo/task1indicates to executetask1in namespacefooxrun ?task1orxrun ?foo/task1indicates that executingtask1is optional.
xrun treats these characters as special:
/as namespace separator- prefix
?to let you indicate that the execution of a task is optional so it won't fail if the task is not found. - prefix
~$to indicate the task to be a string as a shell command
By prefixing the task name with ? when invoking, you can indicate the execution of a task as optional so it won't fail in case the task is not found.
For example:
xrun ?foo/task1orxrun ?task1won't fail iftask1is not found.
A task can be string, array, function, or object. See reference for details.
You can define @xarc/run tasks and options in your package.json.
You can also define xrun tasks without JavaScript capability in your package.json.
They will be loaded into a namespace pkg.
For example:
{
"name": "my-app",
"@xarc/run": {
"tasks": {
"task1": "echo hello from package.json",
"task2": "echo hello from package.json",
"foo": ["task1", "task2"]
}
}
}And you can invoke them with xrun pkg/foo, or xrun foo if there are no other namespace with a task named foo.
Command line options can also be specified under @xarc/run or xrun inside your package.json.
For example:
{
"name": "my-app",
"@xarc/run": {
"npm": true
}
}You can provide a JS function for a task that executes asynchronously. Your function just need to take a callback or return a Promise or a node.js stream.
ie:
const tasks = {
cb_async: (cb) => {
setTimeout(cb, 10);
},
promise_async: () => {
return new Promise(resolve => {
setTimeout(resolve, 10);
}
}
}See reference for more detailed information on features such as load tasks into namespace, and setup auto complete with namespace for your shell.
Licensed under the Apache License, Version 2.0