When the ipc
option is true
, the current process and subprocess can exchange messages. This only works if the subprocess is a Node.js file.
The ipc
option defaults to true
when using execaNode()
or the node
option.
The current process sends messages with subprocess.sendMessage(message)
and receives them with subprocess.getOneMessage()
.
The subprocess uses sendMessage(message)
and getOneMessage()
. Those are the same methods, but imported directly from the 'execa'
module.
// parent.js
import {execaNode} from 'execa';
const subprocess = execaNode`child.js`;
await subprocess.sendMessage('Hello from parent');
const message = await subprocess.getOneMessage();
console.log(message); // 'Hello from child'
await subprocess;
// child.js
import {getOneMessage, sendMessage} from 'execa';
const message = await getOneMessage(); // 'Hello from parent'
const newMessage = message.replace('parent', 'child'); // 'Hello from child'
await sendMessage(newMessage);
The methods described above read a single message. On the other hand, subprocess.getEachMessage()
and getEachMessage()
return an async iterable. This should be preferred when listening to multiple messages.
subprocess.getEachMessage()
waits for the subprocess to end (even when using break
or return
). It throws if the subprocess fails. This means you do not need to await
the subprocess' promise.
// parent.js
import {execaNode} from 'execa';
const subprocess = execaNode`child.js`;
await subprocess.sendMessage(0);
// This loop ends when the subprocess exits.
// It throws if the subprocess fails.
for await (const message of subprocess.getEachMessage()) {
console.log(message); // 1, 3, 5, 7, 9
await subprocess.sendMessage(message + 1);
}
// child.js
import {sendMessage, getEachMessage} from 'execa';
// The subprocess exits when hitting `break`
for await (const message of getEachMessage()) {
if (message === 10) {
break;
}
console.log(message); // 0, 2, 4, 6, 8
await sendMessage(message + 1);
}
import {getOneMessage} from 'execa';
const startMessage = await getOneMessage({
filter: message => message.type === 'start',
});
import {getEachMessage} from 'execa';
for await (const message of getEachMessage()) {
if (message.type === 'start') {
// ...
}
}
When a message is sent by one process, the other process must receive it using getOneMessage()
, getEachMessage()
, or automatically with result.ipcOutput
. If not, that message is silently discarded.
If the strict: true
option is passed to subprocess.sendMessage(message)
or sendMessage(message)
, an error is thrown instead. This helps identifying subtle race conditions like the following example.
// main.js
import {execaNode} from 'execa';
const subprocess = execaNode`build.js`;
// This `build` message is received
await subprocess.sendMessage('build', {strict: true});
// This `lint` message is not received, so it throws
await subprocess.sendMessage('lint', {strict: true});
await subprocess;
// build.js
import {getOneMessage} from 'execa';
// Receives the 'build' message
const task = await getOneMessage();
// The `lint` message is sent while `runTask()` is ongoing
// Therefore the `lint` message is discarded
await runTask(task);
// Does not receive the `lint` message
// Without `strict`, this would wait forever
const secondTask = await getOneMessage();
await runTask(secondTask);
The result.ipcOutput
array contains all the messages sent by the subprocess. In many situations, this is simpler than using subprocess.getOneMessage()
and subprocess.getEachMessage()
.
// main.js
import {execaNode} from 'execa';
const {ipcOutput} = await execaNode`build.js`;
console.log(ipcOutput[0]); // {kind: 'start', timestamp: date}
console.log(ipcOutput[1]); // {kind: 'stop', timestamp: date}
// build.js
import {sendMessage} from 'execa';
await sendMessage({kind: 'start', timestamp: new Date()});
await runBuild();
await sendMessage({kind: 'stop', timestamp: new Date()});
The ipcInput
option sends a message to the Node.js subprocess when it starts.
// main.js
import {execaNode} from 'execa';
const ipcInput = [
{task: 'lint', ignore: /test\.js/},
{task: 'copy', files: new Set(['main.js', 'index.js']),
}];
await execaNode({ipcInput})`build.js`;
// build.js
import {getOneMessage} from 'execa';
const ipcInput = await getOneMessage();
By default, messages are serialized using structuredClone()
. This supports most types including objects, arrays, Error
, Date
, RegExp
, Map
, Set
, bigint
, Uint8Array
, and circular references. This throws when passing functions, symbols or promises (including inside an object or array).
To limit messages to JSON instead, the serialization
option can be set to 'json'
.
import {execaNode} from 'execa';
await execaNode({serialization: 'json'})`child.js`;
The messages are always received in the same order they were sent. Even when sent all at once.
import {sendMessage} from 'execa';
await Promise.all([
sendMessage('first'),
sendMessage('second'),
sendMessage('third'),
]);
By default, the subprocess is kept alive as long as getOneMessage()
or getEachMessage()
is waiting. This is recommended if you're sure the current process will send a message, as this prevents the subprocess from exiting too early.
However, if you don't know whether a message will be sent, this can leave the subprocess hanging forever. In that case, the reference: false
option can be set.
import {getEachMessage} from 'execa';
// {type: 'gracefulExit'} is sometimes received, but not always
for await (const message of getEachMessage({reference: false})) {
if (message.type === 'gracefulExit') {
gracefulExit();
}
}
When the verbose
option is 'full'
, the IPC messages sent by the subprocess to the current process are printed on the console.
Also, when the subprocess failed, error.ipcOutput
contains all the messages sent by the subprocess. Those are also shown at the end of the error message.
Next: 🐛 Debugging
Previous: ⏳️ Streams
Top: Table of contents