@shopify/cli-kit
Version:
A set of utilities, interfaces, and models that are common across all the platform features
154 lines • 6.99 kB
JavaScript
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Box, Static, Text, useApp } from 'ink';
import figures from 'figures';
import stripAnsi from 'strip-ansi';
import { Writable } from 'stream';
import { AsyncLocalStorage } from 'node:async_hooks';
function addLeadingZero(number) {
if (number < 10) {
return `0${number}`;
}
else {
return number.toString();
}
}
function currentTime() {
const currentDateTime = new Date();
const hours = addLeadingZero(currentDateTime.getHours());
const minutes = addLeadingZero(currentDateTime.getMinutes());
const seconds = addLeadingZero(currentDateTime.getSeconds());
return `${hours}:${minutes}:${seconds}`;
}
const outputContextStore = new AsyncLocalStorage();
function useConcurrentOutputContext(context, callback) {
return outputContextStore.run(context, callback);
}
/**
* Renders output from concurrent processes to the terminal.
* Output will be divided in a three column layout
* with the left column containing the timestamp,
* the right column containing the output,
* and the middle column containing the process prefix.
* Every process will be rendered with a different color, up to 4 colors.
*
* For example running `shopify app dev`:
*
* ```shell
* 2022-10-10 13:11:03 | backend | npm
* 2022-10-10 13:11:03 | backend | WARN ignoring workspace config at ...
* 2022-10-10 13:11:03 | backend |
* 2022-10-10 13:11:03 | backend |
* 2022-10-10 13:11:03 | backend | > shopify-app-template-node@0.1.0 dev
* 2022-10-10 13:11:03 | backend | > cross-env NODE_ENV=development nodemon backend/index.js --watch ./backend
* 2022-10-10 13:11:03 | backend |
* 2022-10-10 13:11:03 | backend |
* 2022-10-10 13:11:03 | frontend |
* 2022-10-10 13:11:03 | frontend | > starter-react-frontend-app@0.1.0 dev
* 2022-10-10 13:11:03 | frontend | > cross-env NODE_ENV=development node vite-server.js
* 2022-10-10 13:11:03 | frontend |
* 2022-10-10 13:11:03 | frontend |
* 2022-10-10 13:11:03 | backend |
* 2022-10-10 13:11:03 | backend | [nodemon] to restart at any time, enter `rs`
* 2022-10-10 13:11:03 | backend | [nodemon] watching path(s): backend/
* 2022-10-10 13:11:03 | backend | [nodemon] watching extensions: js,mjs,json
* 2022-10-10 13:11:03 | backend | [nodemon] starting `node backend/index.js`
* 2022-10-10 13:11:03 | backend |
*
* ```
*/
const ConcurrentOutput = ({ processes, prefixColumnSize, abortSignal, showTimestamps = true, keepRunningAfterProcessesResolve = false, useAlternativeColorPalette = false, }) => {
const [processOutput, setProcessOutput] = useState([]);
const { exit: unmountInk } = useApp();
const concurrentColors = useMemo(() => useAlternativeColorPalette
? ['#b994c3', '#e69e19', '#d17a73', 'cyan', 'magenta', 'blue']
: ['yellow', 'cyan', 'magenta', 'green', 'blue'], [useAlternativeColorPalette]);
const calculatedPrefixColumnSize = useMemo(() => {
const maxColumnSize = 25;
// If the prefixColumnSize is not provided, we calculate it based on the longest process prefix
const columnSize = prefixColumnSize ??
processes.reduce((maxPrefixLength, process) => Math.max(maxPrefixLength, process.prefix.length), 0);
// Apply overall limit to the prefix column size
return Math.min(columnSize, maxColumnSize);
}, [processes, prefixColumnSize]);
const addPrefix = (prefix, prefixes) => {
const index = prefixes.indexOf(prefix);
if (index !== -1) {
return index;
}
prefixes.push(prefix);
return prefixes.length - 1;
};
const lineColor = useCallback((index) => {
const colorIndex = index % concurrentColors.length;
return concurrentColors[colorIndex];
}, [concurrentColors]);
const writableStream = useCallback((process, prefixes) => {
return new Writable({
write(chunk, _encoding, next) {
const context = outputContextStore.getStore();
const prefix = context?.outputPrefix ?? process.prefix;
const shouldStripAnsi = context?.stripAnsi ?? true;
const log = chunk.toString('utf8').replace(/(\n)$/, '');
const index = addPrefix(prefix, prefixes);
const lines = shouldStripAnsi ? stripAnsi(log).split(/\n/) : log.split(/\n/);
setProcessOutput((previousProcessOutput) => [
...previousProcessOutput,
{
color: lineColor(index),
prefix,
lines,
},
]);
next();
},
});
}, [lineColor]);
const formatPrefix = (prefix) => {
// Truncate prefix if needed
if (prefix.length > calculatedPrefixColumnSize) {
return prefix.substring(0, calculatedPrefixColumnSize);
}
return `${' '.repeat(calculatedPrefixColumnSize - prefix.length)}${prefix}`;
};
useEffect(() => {
const runProcesses = async () => {
const prefixes = [];
try {
await Promise.all(processes.map(async (process) => {
const stdout = writableStream(process, prefixes);
const stderr = writableStream(process, prefixes);
await process.action(stdout, stderr, abortSignal);
}));
if (!keepRunningAfterProcessesResolve) {
unmountInk();
}
// eslint-disable-next-line no-catch-all/no-catch-all
}
catch (error) {
if (!keepRunningAfterProcessesResolve) {
unmountInk(error);
}
}
};
// eslint-disable-next-line @typescript-eslint/no-floating-promises
runProcesses();
}, [abortSignal, processes, writableStream, unmountInk, keepRunningAfterProcessesResolve]);
const { lineVertical } = figures;
return (React.createElement(Static, { items: processOutput }, (chunk, index) => {
return (React.createElement(Box, { flexDirection: "column", key: index }, chunk.lines.map((line, index) => (React.createElement(Box, { key: index, flexDirection: "row" },
React.createElement(Text, null,
showTimestamps ? (React.createElement(Text, null,
currentTime(),
" ",
lineVertical,
' ')) : null,
React.createElement(Text, { color: chunk.color }, formatPrefix(chunk.prefix)),
React.createElement(Text, null,
' ',
lineVertical,
" ",
line)))))));
}));
};
export { ConcurrentOutput, useConcurrentOutputContext };
//# sourceMappingURL=ConcurrentOutput.js.map