npm run
enhanced - A powerful task runner and build tool for modern JavaScript projects.
-
Compatible with
npm run
for 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 &&
|
-
exec
supportsoptions
that 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
xrun
will wait for the Promise. - Return a stream and
xrun
will wait for the stream to end. - Return another task for
xrun
to 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"
});
xrun
addsnode_modules/.bin
to PATH. That's whynpx
is not needed to run commands likecypress
that'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
xrunProvider
config in itspackage.json
- Having
@xarc/run
as 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: true
in your@xarc/run
config
Provider tasks are loaded from:
dependencies
devDependencies
optionalDependencies
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 serially
Not 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/run
However, 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
run
isasyncRun
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 typescript
xrun
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 build
You 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 -h
See 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 test
You 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 xrun
execute the tasks [ task_a, task_b ]
concurrently:
$ xrun [ task_a, task_b ]
You can also execute them serially with:
$ xrun --serial [ task_a, task_b ]
You can execute tasks serially, and then some tasks concurrently:
$ xrun --serial [task_a, task_b, [task_c1, task_c2]]
will execute
task_a
, thentask_b
, and finallytask_c1
andtask_c2
concurrently.
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/task1
indicates to executetask1
in namespacefoo
-
xrun ?task1
orxrun ?foo/task1
indicates that executingtask1
is 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/task1
orxrun ?task1
won't fail iftask1
is 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