@jsenv/core
Version:
Tool to develop, test and build js projects
1,890 lines (1,704 loc) • 306 kB
JavaScript
import { createSupportsColor, isUnicodeSupported, stripAnsi, emojiRegex, eastAsianWidth, clearTerminal, eraseLines } from "./jsenv_core_node_modules.js";
import { extname } from "node:path";
import { existsSync, readFileSync as readFileSync$1, readdir, chmod, stat, lstat, chmodSync, statSync, lstatSync, promises, readdirSync, openSync, closeSync, unlinkSync, rmdirSync, mkdirSync, writeFileSync as writeFileSync$1, unlink, rmdir, watch, realpathSync } from "node:fs";
import crypto, { createHash } from "node:crypto";
import { pathToFileURL, fileURLToPath } from "node:url";
import { cpus, totalmem, freemem } from "node:os";
import { cpuUsage, memoryUsage } from "node:process";
import { stripVTControlCharacters } from "node:util";
const createCallbackListNotifiedOnce = () => {
let callbacks = [];
let status = "waiting";
let currentCallbackIndex = -1;
const callbackListOnce = {};
const add = (callback) => {
if (status !== "waiting") {
emitUnexpectedActionWarning({ action: "add", status });
return removeNoop;
}
if (typeof callback !== "function") {
throw new Error(`callback must be a function, got ${callback}`);
}
// don't register twice
const existingCallback = callbacks.find((callbackCandidate) => {
return callbackCandidate === callback;
});
if (existingCallback) {
emitCallbackDuplicationWarning();
return removeNoop;
}
callbacks.push(callback);
return () => {
if (status === "notified") {
// once called removing does nothing
// as the callbacks array is frozen to null
return;
}
const index = callbacks.indexOf(callback);
if (index === -1) {
return;
}
if (status === "looping") {
if (index <= currentCallbackIndex) {
// The callback was already called (or is the current callback)
// We don't want to mutate the callbacks array
// or it would alter the looping done in "call" and the next callback
// would be skipped
return;
}
// Callback is part of the next callback to call,
// we mutate the callbacks array to prevent this callback to be called
}
callbacks.splice(index, 1);
};
};
const notify = (param) => {
if (status !== "waiting") {
emitUnexpectedActionWarning({ action: "call", status });
return [];
}
status = "looping";
const values = callbacks.map((callback, index) => {
currentCallbackIndex = index;
return callback(param);
});
callbackListOnce.notified = true;
status = "notified";
// we reset callbacks to null after looping
// so that it's possible to remove during the loop
callbacks = null;
currentCallbackIndex = -1;
return values;
};
callbackListOnce.notified = false;
callbackListOnce.add = add;
callbackListOnce.notify = notify;
return callbackListOnce;
};
const emitUnexpectedActionWarning = ({ action, status }) => {
if (typeof process.emitWarning === "function") {
process.emitWarning(
`"${action}" should not happen when callback list is ${status}`,
{
CODE: "UNEXPECTED_ACTION_ON_CALLBACK_LIST",
detail: `Code is potentially executed when it should not`,
},
);
} else {
console.warn(
`"${action}" should not happen when callback list is ${status}`,
);
}
};
const emitCallbackDuplicationWarning = () => {
if (typeof process.emitWarning === "function") {
process.emitWarning(`Trying to add a callback already in the list`, {
CODE: "CALLBACK_DUPLICATION",
detail: `Code is potentially executed more than it should`,
});
} else {
console.warn(`Trying to add same callback twice`);
}
};
const removeNoop = () => {};
/*
* See callback_race.md
*/
const raceCallbacks = (raceDescription, winnerCallback) => {
let cleanCallbacks = [];
let status = "racing";
const clean = () => {
cleanCallbacks.forEach((clean) => {
clean();
});
cleanCallbacks = null;
};
const cancel = () => {
if (status !== "racing") {
return;
}
status = "cancelled";
clean();
};
Object.keys(raceDescription).forEach((candidateName) => {
const register = raceDescription[candidateName];
const returnValue = register((data) => {
if (status !== "racing") {
return;
}
status = "done";
clean();
winnerCallback({
name: candidateName,
data,
});
});
if (typeof returnValue === "function") {
cleanCallbacks.push(returnValue);
}
});
return cancel;
};
/*
* https://github.com/whatwg/dom/issues/920
*/
const Abort = {
isAbortError: (error) => {
return error && error.name === "AbortError";
},
startOperation: () => {
return createOperation();
},
throwIfAborted: (signal) => {
if (signal.aborted) {
const error = new Error(`The operation was aborted`);
error.name = "AbortError";
error.type = "aborted";
throw error;
}
},
};
const createOperation = () => {
const operationAbortController = new AbortController();
// const abortOperation = (value) => abortController.abort(value)
const operationSignal = operationAbortController.signal;
// abortCallbackList is used to ignore the max listeners warning from Node.js
// this warning is useful but becomes problematic when it's expect
// (a function doing 20 http call in parallel)
// To be 100% sure we don't have memory leak, only Abortable.asyncCallback
// uses abortCallbackList to know when something is aborted
const abortCallbackList = createCallbackListNotifiedOnce();
const endCallbackList = createCallbackListNotifiedOnce();
let isAbortAfterEnd = false;
operationSignal.onabort = () => {
operationSignal.onabort = null;
const allAbortCallbacksPromise = Promise.all(abortCallbackList.notify());
if (!isAbortAfterEnd) {
addEndCallback(async () => {
await allAbortCallbacksPromise;
});
}
};
const throwIfAborted = () => {
Abort.throwIfAborted(operationSignal);
};
// add a callback called on abort
// differences with signal.addEventListener('abort')
// - operation.end awaits the return value of this callback
// - It won't increase the count of listeners for "abort" that would
// trigger max listeners warning when count > 10
const addAbortCallback = (callback) => {
// It would be painful and not super redable to check if signal is aborted
// before deciding if it's an abort or end callback
// with pseudo-code below where we want to stop server either
// on abort or when ended because signal is aborted
// operation[operation.signal.aborted ? 'addAbortCallback': 'addEndCallback'](async () => {
// await server.stop()
// })
if (operationSignal.aborted) {
return addEndCallback(callback);
}
return abortCallbackList.add(callback);
};
const addEndCallback = (callback) => {
return endCallbackList.add(callback);
};
const end = async ({ abortAfterEnd = false } = {}) => {
await Promise.all(endCallbackList.notify());
// "abortAfterEnd" can be handy to ensure "abort" callbacks
// added with { once: true } are removed
// It might also help garbage collection because
// runtime implementing AbortSignal (Node.js, browsers) can consider abortSignal
// as settled and clean up things
if (abortAfterEnd) {
// because of operationSignal.onabort = null
// + abortCallbackList.clear() this won't re-call
// callbacks
if (!operationSignal.aborted) {
isAbortAfterEnd = true;
operationAbortController.abort();
}
}
};
const addAbortSignal = (
signal,
{ onAbort = callbackNoop, onRemove = callbackNoop } = {},
) => {
const applyAbortEffects = () => {
const onAbortCallback = onAbort;
onAbort = callbackNoop;
onAbortCallback();
};
const applyRemoveEffects = () => {
const onRemoveCallback = onRemove;
onRemove = callbackNoop;
onAbort = callbackNoop;
onRemoveCallback();
};
if (operationSignal.aborted) {
applyAbortEffects();
applyRemoveEffects();
return callbackNoop;
}
if (signal.aborted) {
operationAbortController.abort();
applyAbortEffects();
applyRemoveEffects();
return callbackNoop;
}
const cancelRace = raceCallbacks(
{
operation_abort: (cb) => {
return addAbortCallback(cb);
},
operation_end: (cb) => {
return addEndCallback(cb);
},
child_abort: (cb) => {
return addEventListener(signal, "abort", cb);
},
},
(winner) => {
const raceEffects = {
// Both "operation_abort" and "operation_end"
// means we don't care anymore if the child aborts.
// So we can:
// - remove "abort" event listener on child (done by raceCallback)
// - remove abort callback on operation (done by raceCallback)
// - remove end callback on operation (done by raceCallback)
// - call any custom cancel function
operation_abort: () => {
applyAbortEffects();
applyRemoveEffects();
},
operation_end: () => {
// Exists to
// - remove abort callback on operation
// - remove "abort" event listener on child
// - call any custom cancel function
applyRemoveEffects();
},
child_abort: () => {
applyAbortEffects();
operationAbortController.abort();
},
};
raceEffects[winner.name](winner.value);
},
);
return () => {
cancelRace();
applyRemoveEffects();
};
};
const addAbortSource = (abortSourceCallback) => {
const abortSource = {
cleaned: false,
signal: null,
remove: callbackNoop,
};
const abortSourceController = new AbortController();
const abortSourceSignal = abortSourceController.signal;
abortSource.signal = abortSourceSignal;
if (operationSignal.aborted) {
return abortSource;
}
const returnValue = abortSourceCallback((value) => {
abortSourceController.abort(value);
});
const removeAbortSignal = addAbortSignal(abortSourceSignal, {
onRemove: () => {
if (typeof returnValue === "function") {
returnValue();
}
abortSource.cleaned = true;
},
});
abortSource.remove = removeAbortSignal;
return abortSource;
};
const timeout = (ms) => {
return addAbortSource((abort) => {
const timeoutId = setTimeout(abort, ms);
// an abort source return value is called when:
// - operation is aborted (by an other source)
// - operation ends
return () => {
clearTimeout(timeoutId);
};
});
};
const wait = (ms) => {
return new Promise((resolve) => {
const timeoutId = setTimeout(() => {
removeAbortCallback();
resolve();
}, ms);
const removeAbortCallback = addAbortCallback(() => {
clearTimeout(timeoutId);
});
});
};
const withSignal = async (asyncCallback) => {
const abortController = new AbortController();
const signal = abortController.signal;
const removeAbortSignal = addAbortSignal(signal, {
onAbort: () => {
abortController.abort();
},
});
try {
const value = await asyncCallback(signal);
removeAbortSignal();
return value;
} catch (e) {
removeAbortSignal();
throw e;
}
};
const withSignalSync = (callback) => {
const abortController = new AbortController();
const signal = abortController.signal;
const removeAbortSignal = addAbortSignal(signal, {
onAbort: () => {
abortController.abort();
},
});
try {
const value = callback(signal);
removeAbortSignal();
return value;
} catch (e) {
removeAbortSignal();
throw e;
}
};
const fork = () => {
const forkedOperation = createOperation();
forkedOperation.addAbortSignal(operationSignal);
return forkedOperation;
};
return {
// We could almost hide the operationSignal
// But it can be handy for 2 things:
// - know if operation is aborted (operation.signal.aborted)
// - forward the operation.signal directly (not using "withSignal" or "withSignalSync")
signal: operationSignal,
throwIfAborted,
addAbortCallback,
addAbortSignal,
addAbortSource,
fork,
timeout,
wait,
withSignal,
withSignalSync,
addEndCallback,
end,
};
};
const callbackNoop = () => {};
const addEventListener = (target, eventName, cb) => {
target.addEventListener(eventName, cb);
return () => {
target.removeEventListener(eventName, cb);
};
};
const raceProcessTeardownEvents = (processTeardownEvents, callback) => {
return raceCallbacks(
{
...(processTeardownEvents.SIGHUP ? SIGHUP_CALLBACK : {}),
...(processTeardownEvents.SIGTERM ? SIGTERM_CALLBACK : {}),
...(SIGINT_CALLBACK ),
...(processTeardownEvents.beforeExit ? BEFORE_EXIT_CALLBACK : {}),
...(processTeardownEvents.exit ? EXIT_CALLBACK : {}),
},
callback,
);
};
const SIGHUP_CALLBACK = {
SIGHUP: (cb) => {
process.on("SIGHUP", cb);
return () => {
process.removeListener("SIGHUP", cb);
};
},
};
const SIGTERM_CALLBACK = {
SIGTERM: (cb) => {
process.on("SIGTERM", cb);
return () => {
process.removeListener("SIGTERM", cb);
};
},
};
const BEFORE_EXIT_CALLBACK = {
beforeExit: (cb) => {
process.on("beforeExit", cb);
return () => {
process.removeListener("beforeExit", cb);
};
},
};
const EXIT_CALLBACK = {
exit: (cb) => {
process.on("exit", cb);
return () => {
process.removeListener("exit", cb);
};
},
};
const SIGINT_CALLBACK = {
SIGINT: (cb) => {
process.on("SIGINT", cb);
return () => {
process.removeListener("SIGINT", cb);
};
},
};
/*
* data:[<mediatype>][;base64],<data>
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs#syntax
*/
/* eslint-env browser, node */
const DATA_URL = {
parse: (string) => {
const afterDataProtocol = string.slice("data:".length);
const commaIndex = afterDataProtocol.indexOf(",");
const beforeComma = afterDataProtocol.slice(0, commaIndex);
let contentType;
let base64Flag;
if (beforeComma.endsWith(`;base64`)) {
contentType = beforeComma.slice(0, -`;base64`.length);
base64Flag = true;
} else {
contentType = beforeComma;
base64Flag = false;
}
contentType =
contentType === "" ? "text/plain;charset=US-ASCII" : contentType;
const afterComma = afterDataProtocol.slice(commaIndex + 1);
return {
contentType,
base64Flag,
data: afterComma,
};
},
stringify: ({ contentType, base64Flag = true, data }) => {
if (!contentType || contentType === "text/plain;charset=US-ASCII") {
// can be a buffer or a string, hence check on data.length instead of !data or data === ''
if (data.length === 0) {
return `data:,`;
}
if (base64Flag) {
return `data:;base64,${data}`;
}
return `data:,${data}`;
}
if (base64Flag) {
return `data:${contentType};base64,${data}`;
}
return `data:${contentType},${data}`;
},
};
const createDetailedMessage$1 = (message, details = {}) => {
let text = `${message}`;
const namedSectionsText = renderNamedSections(details);
if (namedSectionsText) {
text += `
${namedSectionsText}`;
}
return text;
};
const renderNamedSections = (namedSections) => {
let text = "";
let keys = Object.keys(namedSections);
for (const key of keys) {
const isLastKey = key === keys[keys.length - 1];
const value = namedSections[key];
text += `--- ${key} ---
${
Array.isArray(value)
? value.join(`
`)
: value
}`;
if (!isLastKey) {
text += "\n";
}
}
return text;
};
// https://github.com/Marak/colors.js/blob/master/lib/styles.js
// https://stackoverflow.com/a/75985833/2634179
const RESET = "\x1b[0m";
const RED = "red";
const GREEN = "green";
const YELLOW = "yellow";
const BLUE = "blue";
const MAGENTA = "magenta";
const CYAN = "cyan";
const GREY = "grey";
const WHITE = "white";
const BLACK = "black";
const TEXT_COLOR_ANSI_CODES = {
[RED]: "\x1b[31m",
[GREEN]: "\x1b[32m",
[YELLOW]: "\x1b[33m",
[BLUE]: "\x1b[34m",
[MAGENTA]: "\x1b[35m",
[CYAN]: "\x1b[36m",
[GREY]: "\x1b[90m",
[WHITE]: "\x1b[37m",
[BLACK]: "\x1b[30m",
};
const BACKGROUND_COLOR_ANSI_CODES = {
[RED]: "\x1b[41m",
[GREEN]: "\x1b[42m",
[YELLOW]: "\x1b[43m",
[BLUE]: "\x1b[44m",
[MAGENTA]: "\x1b[45m",
[CYAN]: "\x1b[46m",
[GREY]: "\x1b[100m",
[WHITE]: "\x1b[47m",
[BLACK]: "\x1b[40m",
};
const createAnsi = ({ supported }) => {
const ANSI = {
supported,
RED,
GREEN,
YELLOW,
BLUE,
MAGENTA,
CYAN,
GREY,
WHITE,
BLACK,
color: (text, color) => {
if (!ANSI.supported) {
return text;
}
if (!color) {
return text;
}
if (typeof text === "string" && text.trim() === "") {
// cannot set color of blank chars
return text;
}
const ansiEscapeCodeForTextColor = TEXT_COLOR_ANSI_CODES[color];
if (!ansiEscapeCodeForTextColor) {
return text;
}
return `${ansiEscapeCodeForTextColor}${text}${RESET}`;
},
backgroundColor: (text, color) => {
if (!ANSI.supported) {
return text;
}
if (!color) {
return text;
}
if (typeof text === "string" && text.trim() === "") {
// cannot set background color of blank chars
return text;
}
const ansiEscapeCodeForBackgroundColor =
BACKGROUND_COLOR_ANSI_CODES[color];
if (!ansiEscapeCodeForBackgroundColor) {
return text;
}
return `${ansiEscapeCodeForBackgroundColor}${text}${RESET}`;
},
BOLD: "\x1b[1m",
UNDERLINE: "\x1b[4m",
STRIKE: "\x1b[9m",
effect: (text, effect) => {
if (!ANSI.supported) {
return text;
}
if (!effect) {
return text;
}
// cannot add effect to empty string
if (text === "") {
return text;
}
const ansiEscapeCodeForEffect = effect;
return `${ansiEscapeCodeForEffect}${text}${RESET}`;
},
};
return ANSI;
};
const processSupportsBasicColor = createSupportsColor(process.stdout).hasBasic;
const ANSI = createAnsi({
supported:
process.env.FORCE_COLOR === "1" ||
processSupportsBasicColor ||
// GitHub workflow does support ANSI but "supports-color" returns false
// because stream.isTTY returns false, see https://github.com/actions/runner/issues/241
process.env.GITHUB_WORKFLOW,
});
// see also https://github.com/sindresorhus/figures
const createUnicode = ({ supported, ANSI }) => {
const UNICODE = {
supported,
get COMMAND_RAW() {
return UNICODE.supported ? `❯` : `>`;
},
get OK_RAW() {
return UNICODE.supported ? `✔` : `√`;
},
get FAILURE_RAW() {
return UNICODE.supported ? `✖` : `×`;
},
get DEBUG_RAW() {
return UNICODE.supported ? `◆` : `♦`;
},
get INFO_RAW() {
return UNICODE.supported ? `ℹ` : `i`;
},
get WARNING_RAW() {
return UNICODE.supported ? `⚠` : `‼`;
},
get CIRCLE_CROSS_RAW() {
return UNICODE.supported ? `ⓧ` : `(×)`;
},
get CIRCLE_DOTTED_RAW() {
return UNICODE.supported ? `◌` : `*`;
},
get COMMAND() {
return ANSI.color(UNICODE.COMMAND_RAW, ANSI.GREY); // ANSI_MAGENTA)
},
get OK() {
return ANSI.color(UNICODE.OK_RAW, ANSI.GREEN);
},
get FAILURE() {
return ANSI.color(UNICODE.FAILURE_RAW, ANSI.RED);
},
get DEBUG() {
return ANSI.color(UNICODE.DEBUG_RAW, ANSI.GREY);
},
get INFO() {
return ANSI.color(UNICODE.INFO_RAW, ANSI.BLUE);
},
get WARNING() {
return ANSI.color(UNICODE.WARNING_RAW, ANSI.YELLOW);
},
get CIRCLE_CROSS() {
return ANSI.color(UNICODE.CIRCLE_CROSS_RAW, ANSI.RED);
},
get ELLIPSIS() {
return UNICODE.supported ? `…` : `...`;
},
};
return UNICODE;
};
const UNICODE = createUnicode({
supported: process.env.FORCE_UNICODE === "1" || isUnicodeSupported(),
ANSI,
});
const getPrecision = (number) => {
if (Math.floor(number) === number) return 0;
const [, decimals] = number.toString().split(".");
return decimals.length || 0;
};
const setRoundedPrecision = (
number,
{ decimals = 1, decimalsWhenSmall = decimals } = {},
) => {
return setDecimalsPrecision(number, {
decimals,
decimalsWhenSmall,
transform: Math.round,
});
};
const setPrecision = (
number,
{ decimals = 1, decimalsWhenSmall = decimals } = {},
) => {
return setDecimalsPrecision(number, {
decimals,
decimalsWhenSmall,
transform: parseInt,
});
};
const setDecimalsPrecision = (
number,
{
transform,
decimals, // max decimals for number in [-Infinity, -1[]1, Infinity]
decimalsWhenSmall, // max decimals for number in [-1,1]
} = {},
) => {
if (number === 0) {
return 0;
}
let numberCandidate = Math.abs(number);
if (numberCandidate < 1) {
const integerGoal = Math.pow(10, decimalsWhenSmall - 1);
let i = 1;
while (numberCandidate < integerGoal) {
numberCandidate *= 10;
i *= 10;
}
const asInteger = transform(numberCandidate);
const asFloat = asInteger / i;
return number < 0 ? -asFloat : asFloat;
}
const coef = Math.pow(10, decimals);
const numberMultiplied = (number + Number.EPSILON) * coef;
const asInteger = transform(numberMultiplied);
const asFloat = asInteger / coef;
return number < 0 ? -asFloat : asFloat;
};
// https://www.codingem.com/javascript-how-to-limit-decimal-places/
// export const roundNumber = (number, maxDecimals) => {
// const decimalsExp = Math.pow(10, maxDecimals)
// const numberRoundInt = Math.round(decimalsExp * (number + Number.EPSILON))
// const numberRoundFloat = numberRoundInt / decimalsExp
// return numberRoundFloat
// }
// export const setPrecision = (number, precision) => {
// if (Math.floor(number) === number) return number
// const [int, decimals] = number.toString().split(".")
// if (precision <= 0) return int
// const numberTruncated = `${int}.${decimals.slice(0, precision)}`
// return numberTruncated
// }
const unitShort = {
year: "y",
month: "m",
week: "w",
day: "d",
hour: "h",
minute: "m",
second: "s",
};
const humanizeDuration = (
ms,
{ short, rounded = true, decimals } = {},
) => {
// ignore ms below meaningfulMs so that:
// humanizeDuration(0.5) -> "0 second"
// humanizeDuration(1.1) -> "0.001 second" (and not "0.0011 second")
// This tool is meant to be read by humans and it would be barely readable to see
// "0.0001 second" (stands for 0.1 millisecond)
// yes we could return "0.1 millisecond" but we choosed consistency over precision
// so that the prefered unit is "second" (and does not become millisecond when ms is super small)
if (ms < 1) {
return short ? "0s" : "0 second";
}
const { primary, remaining } = parseMs(ms);
if (!remaining) {
return humanizeDurationUnit(primary, {
decimals:
decimals === undefined ? (primary.name === "second" ? 1 : 0) : decimals,
short,
rounded,
});
}
return `${humanizeDurationUnit(primary, {
decimals: decimals === undefined ? 0 : decimals,
short,
rounded,
})} and ${humanizeDurationUnit(remaining, {
decimals: decimals === undefined ? 0 : decimals,
short,
rounded,
})}`;
};
const humanizeDurationUnit = (unit, { decimals, short, rounded }) => {
const count = rounded
? setRoundedPrecision(unit.count, { decimals })
: setPrecision(unit.count, { decimals });
let name = unit.name;
if (short) {
name = unitShort[name];
return `${count}${name}`;
}
if (count <= 1) {
return `${count} ${name}`;
}
return `${count} ${name}s`;
};
const MS_PER_UNITS = {
year: 31_557_600_000,
month: 2_629_000_000,
week: 604_800_000,
day: 86_400_000,
hour: 3_600_000,
minute: 60_000,
second: 1000,
};
const parseMs = (ms) => {
const unitNames = Object.keys(MS_PER_UNITS);
const smallestUnitName = unitNames[unitNames.length - 1];
let firstUnitName = smallestUnitName;
let firstUnitCount = ms / MS_PER_UNITS[smallestUnitName];
const firstUnitIndex = unitNames.findIndex((unitName) => {
if (unitName === smallestUnitName) {
return false;
}
const msPerUnit = MS_PER_UNITS[unitName];
const unitCount = Math.floor(ms / msPerUnit);
if (unitCount) {
firstUnitName = unitName;
firstUnitCount = unitCount;
return true;
}
return false;
});
if (firstUnitName === smallestUnitName) {
return {
primary: {
name: firstUnitName,
count: firstUnitCount,
},
};
}
const remainingMs = ms - firstUnitCount * MS_PER_UNITS[firstUnitName];
const remainingUnitName = unitNames[firstUnitIndex + 1];
const remainingUnitCount = remainingMs / MS_PER_UNITS[remainingUnitName];
// - 1 year and 1 second is too much information
// so we don't check the remaining units
// - 1 year and 0.0001 week is awful
// hence the if below
if (Math.round(remainingUnitCount) < 1) {
return {
primary: {
name: firstUnitName,
count: firstUnitCount,
},
};
}
// - 1 year and 1 month is great
return {
primary: {
name: firstUnitName,
count: firstUnitCount,
},
remaining: {
name: remainingUnitName,
count: remainingUnitCount,
},
};
};
const humanizeFileSize = (numberOfBytes, { decimals, short } = {}) => {
return inspectBytes(numberOfBytes, { decimals, short });
};
const humanizeMemory = (metricValue, { decimals, short } = {}) => {
return inspectBytes(metricValue, { decimals, fixedDecimals: true, short });
};
const inspectBytes = (
number,
{ fixedDecimals = false, decimals, short } = {},
) => {
if (number === 0) {
return `0 B`;
}
const exponent = Math.min(
Math.floor(Math.log10(number) / 3),
BYTE_UNITS.length - 1,
);
const unitNumber = number / Math.pow(1000, exponent);
const unitName = BYTE_UNITS[exponent];
if (decimals === undefined) {
if (unitNumber < 100) {
decimals = 1;
} else {
decimals = 0;
}
}
const unitNumberRounded = setRoundedPrecision(unitNumber, {
decimals,
decimalsWhenSmall: 1,
});
const value = fixedDecimals
? unitNumberRounded.toFixed(decimals)
: unitNumberRounded;
if (short) {
return `${value}${unitName}`;
}
return `${value} ${unitName}`;
};
const BYTE_UNITS = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const distributePercentages = (
namedNumbers,
{ maxPrecisionHint = 2 } = {},
) => {
const numberNames = Object.keys(namedNumbers);
if (numberNames.length === 0) {
return {};
}
if (numberNames.length === 1) {
const firstNumberName = numberNames[0];
return { [firstNumberName]: "100 %" };
}
const numbers = numberNames.map((name) => namedNumbers[name]);
const total = numbers.reduce((sum, value) => sum + value, 0);
const ratios = numbers.map((number) => number / total);
const percentages = {};
ratios.pop();
ratios.forEach((ratio, index) => {
const percentage = ratio * 100;
percentages[numberNames[index]] = percentage;
});
const lowestPercentage = (1 / Math.pow(10, maxPrecisionHint)) * 100;
let precision = 0;
Object.keys(percentages).forEach((name) => {
const percentage = percentages[name];
if (percentage < lowestPercentage) {
// check the amout of meaningful decimals
// and that what we will use
const percentageRounded = setRoundedPrecision(percentage);
const percentagePrecision = getPrecision(percentageRounded);
if (percentagePrecision > precision) {
precision = percentagePrecision;
}
}
});
let remainingPercentage = 100;
Object.keys(percentages).forEach((name) => {
const percentage = percentages[name];
const percentageAllocated = setRoundedPrecision(percentage, {
decimals: precision,
});
remainingPercentage -= percentageAllocated;
percentages[name] = percentageAllocated;
});
const lastName = numberNames[numberNames.length - 1];
percentages[lastName] = setRoundedPrecision(remainingPercentage, {
decimals: precision,
});
return percentages;
};
const formatDefault = (v) => v;
const generateContentFrame = ({
content,
line,
column,
linesAbove = 3,
linesBelow = 0,
lineMaxWidth = 120,
lineNumbersOnTheLeft = true,
lineMarker = true,
columnMarker = true,
format = formatDefault,
} = {}) => {
const lineStrings = content.split(/\r?\n/);
if (line === 0) line = 1;
if (column === undefined) {
columnMarker = false;
column = 1;
}
if (column === 0) column = 1;
let lineStartIndex = line - 1 - linesAbove;
if (lineStartIndex < 0) {
lineStartIndex = 0;
}
let lineEndIndex = line - 1 + linesBelow;
if (lineEndIndex > lineStrings.length - 1) {
lineEndIndex = lineStrings.length - 1;
}
if (columnMarker) {
// human reader deduce the line when there is a column marker
lineMarker = false;
}
if (line - 1 === lineEndIndex) {
lineMarker = false; // useless because last line
}
let lineIndex = lineStartIndex;
let columnsBefore;
let columnsAfter;
if (column > lineMaxWidth) {
columnsBefore = column - Math.ceil(lineMaxWidth / 2);
columnsAfter = column + Math.floor(lineMaxWidth / 2);
} else {
columnsBefore = 0;
columnsAfter = lineMaxWidth;
}
let columnMarkerIndex = column - 1 - columnsBefore;
let source = "";
while (lineIndex <= lineEndIndex) {
const lineString = lineStrings[lineIndex];
const lineNumber = lineIndex + 1;
const isLastLine = lineIndex === lineEndIndex;
const isMainLine = lineNumber === line;
lineIndex++;
{
if (lineMarker) {
if (isMainLine) {
source += `${format(">", "marker_line")} `;
} else {
source += " ";
}
}
if (lineNumbersOnTheLeft) {
// fill with spaces to ensure if line moves from 7,8,9 to 10 the display is still great
const asideSource = `${fillLeft(lineNumber, lineEndIndex + 1)} |`;
source += `${format(asideSource, "line_number_aside")} `;
}
}
{
source += truncateLine(lineString, {
start: columnsBefore,
end: columnsAfter,
prefix: "…",
suffix: "…",
format,
});
}
{
if (columnMarker && isMainLine) {
source += `\n`;
if (lineMarker) {
source += " ";
}
if (lineNumbersOnTheLeft) {
const asideSpaces = `${fillLeft(lineNumber, lineEndIndex + 1)} | `
.length;
source += " ".repeat(asideSpaces);
}
source += " ".repeat(columnMarkerIndex);
source += format("^", "marker_column");
}
}
if (!isLastLine) {
source += "\n";
}
}
return source;
};
const truncateLine = (line, { start, end, prefix, suffix, format }) => {
const lastIndex = line.length;
if (line.length === 0) {
// don't show any ellipsis if the line is empty
// because it's not truncated in that case
return "";
}
const startTruncated = start > 0;
const endTruncated = lastIndex > end;
let from = startTruncated ? start + prefix.length : start;
let to = endTruncated ? end - suffix.length : end;
if (to > lastIndex) to = lastIndex;
if (start >= lastIndex || from === to) {
return "";
}
let result = "";
while (from < to) {
result += format(line[from], "char");
from++;
}
if (result.length === 0) {
return "";
}
if (startTruncated && endTruncated) {
return `${format(prefix, "marker_overflow_left")}${result}${format(
suffix,
"marker_overflow_right",
)}`;
}
if (startTruncated) {
return `${format(prefix, "marker_overflow_left")}${result}`;
}
if (endTruncated) {
return `${result}${format(suffix, "marker_overflow_right")}`;
}
return result;
};
const fillLeft = (value, biggestValue, char = " ") => {
const width = String(value).length;
const biggestWidth = String(biggestValue).length;
let missingWidth = biggestWidth - width;
let padded = "";
while (missingWidth--) {
padded += char;
}
padded += value;
return padded;
};
const errorToHTML = (error) => {
const errorIsAPrimitive =
error === null ||
(typeof error !== "object" && typeof error !== "function");
if (errorIsAPrimitive) {
if (typeof error === "string") {
return `<pre>${escapeHtml(error)}</pre>`;
}
return `<pre>${JSON.stringify(error, null, " ")}</pre>`;
}
return `<pre>${escapeHtml(error.stack)}</pre>`;
};
const escapeHtml = (string) => {
return string
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
};
const renderBigSection = (params) => {
return renderSection({
width: 45,
...params,
});
};
const renderSection = ({
title,
content,
dashColor = ANSI.GREY,
width = 38,
bottomSeparator = true,
}) => {
let section = "";
if (title) {
const titleWidth = stripAnsi(title).length;
const minWidthRequired = `--- … ---`.length;
const needsTruncate = titleWidth + minWidthRequired >= width;
if (needsTruncate) {
const titleTruncated = title.slice(0, width - minWidthRequired);
const leftDashes = ANSI.color("---", dashColor);
const rightDashes = ANSI.color("---", dashColor);
section += `${leftDashes} ${titleTruncated}… ${rightDashes}`;
} else {
const remainingWidth = width - titleWidth - 2; // 2 for spaces around the title
const dashLeftCount = Math.floor(remainingWidth / 2);
const dashRightCount = remainingWidth - dashLeftCount;
const leftDashes = ANSI.color("-".repeat(dashLeftCount), dashColor);
const rightDashes = ANSI.color("-".repeat(dashRightCount), dashColor);
section += `${leftDashes} ${title} ${rightDashes}`;
}
section += "\n";
} else {
const topDashes = ANSI.color(`-`.repeat(width), dashColor);
section += topDashes;
section += "\n";
}
section += `${content}`;
if (bottomSeparator) {
section += "\n";
const bottomDashes = ANSI.color(`-`.repeat(width), dashColor);
section += bottomDashes;
}
return section;
};
const renderDetails = (data) => {
const details = [];
for (const key of Object.keys(data)) {
const value = data[key];
let valueString = "";
valueString += ANSI.color(`${key}:`, ANSI.GREY);
const useNonGreyAnsiColor =
typeof value === "string" && value.includes("\x1b");
valueString += " ";
valueString += useNonGreyAnsiColor
? value
: ANSI.color(String(value), ANSI.GREY);
details.push(valueString);
}
if (details.length === 0) {
return "";
}
let string = "";
string += ` ${ANSI.color("(", ANSI.GREY)}`;
string += details.join(ANSI.color(", ", ANSI.GREY));
string += ANSI.color(")", ANSI.GREY);
return string;
};
const LOG_LEVEL_OFF = "off";
const LOG_LEVEL_DEBUG = "debug";
const LOG_LEVEL_INFO = "info";
const LOG_LEVEL_WARN = "warn";
const LOG_LEVEL_ERROR = "error";
const createLogger = ({ logLevel = LOG_LEVEL_INFO } = {}) => {
if (logLevel === LOG_LEVEL_DEBUG) {
return {
level: "debug",
levels: { debug: true, info: true, warn: true, error: true },
debug,
info,
warn,
error,
};
}
if (logLevel === LOG_LEVEL_INFO) {
return {
level: "info",
levels: { debug: false, info: true, warn: true, error: true },
debug: debugDisabled,
info,
warn,
error,
};
}
if (logLevel === LOG_LEVEL_WARN) {
return {
level: "warn",
levels: { debug: false, info: false, warn: true, error: true },
debug: debugDisabled,
info: infoDisabled,
warn,
error,
};
}
if (logLevel === LOG_LEVEL_ERROR) {
return {
level: "error",
levels: { debug: false, info: false, warn: false, error: true },
debug: debugDisabled,
info: infoDisabled,
warn: warnDisabled,
error,
};
}
if (logLevel === LOG_LEVEL_OFF) {
return {
level: "off",
levels: { debug: false, info: false, warn: false, error: false },
debug: debugDisabled,
info: infoDisabled,
warn: warnDisabled,
error: errorDisabled,
};
}
throw new Error(`unexpected logLevel.
--- logLevel ---
${logLevel}
--- allowed log levels ---
${LOG_LEVEL_OFF}
${LOG_LEVEL_ERROR}
${LOG_LEVEL_WARN}
${LOG_LEVEL_INFO}
${LOG_LEVEL_DEBUG}`);
};
const debug = (...args) => console.debug(...args);
const debugDisabled = () => {};
const info = (...args) => console.info(...args);
const infoDisabled = () => {};
const warn = (...args) => console.warn(...args);
const warnDisabled = () => {};
const error = (...args) => console.error(...args);
const errorDisabled = () => {};
const createMeasureTextWidth = ({ stripAnsi }) => {
const segmenter = new Intl.Segmenter();
const defaultIgnorableCodePointRegex = /^\p{Default_Ignorable_Code_Point}$/u;
const measureTextWidth = (
string,
{
ambiguousIsNarrow = true,
countAnsiEscapeCodes = false,
skipEmojis = false,
} = {},
) => {
if (typeof string !== "string" || string.length === 0) {
return 0;
}
if (!countAnsiEscapeCodes) {
string = stripAnsi(string);
}
if (string.length === 0) {
return 0;
}
let width = 0;
const eastAsianWidthOptions = { ambiguousAsWide: !ambiguousIsNarrow };
for (const { segment: character } of segmenter.segment(string)) {
const codePoint = character.codePointAt(0);
// Ignore control characters
if (codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)) {
continue;
}
// Ignore zero-width characters
if (
(codePoint >= 0x20_0b && codePoint <= 0x20_0f) || // Zero-width space, non-joiner, joiner, left-to-right mark, right-to-left mark
codePoint === 0xfe_ff // Zero-width no-break space
) {
continue;
}
// Ignore combining characters
if (
(codePoint >= 0x3_00 && codePoint <= 0x3_6f) || // Combining diacritical marks
(codePoint >= 0x1a_b0 && codePoint <= 0x1a_ff) || // Combining diacritical marks extended
(codePoint >= 0x1d_c0 && codePoint <= 0x1d_ff) || // Combining diacritical marks supplement
(codePoint >= 0x20_d0 && codePoint <= 0x20_ff) || // Combining diacritical marks for symbols
(codePoint >= 0xfe_20 && codePoint <= 0xfe_2f) // Combining half marks
) {
continue;
}
// Ignore surrogate pairs
if (codePoint >= 0xd8_00 && codePoint <= 0xdf_ff) {
continue;
}
// Ignore variation selectors
if (codePoint >= 0xfe_00 && codePoint <= 0xfe_0f) {
continue;
}
// This covers some of the above cases, but we still keep them for performance reasons.
if (defaultIgnorableCodePointRegex.test(character)) {
continue;
}
if (!skipEmojis && emojiRegex().test(character)) {
if (process.env.CAPTURING_SIDE_EFFECTS) {
if (character === "✔️") {
width += 2;
continue;
}
}
width += measureTextWidth(character, {
skipEmojis: true,
countAnsiEscapeCodes: true, // to skip call to stripAnsi
});
continue;
}
width += eastAsianWidth(codePoint, eastAsianWidthOptions);
}
return width;
};
return measureTextWidth;
};
const measureTextWidth = createMeasureTextWidth({
stripAnsi: stripVTControlCharacters,
});
/*
* see also https://github.com/vadimdemedes/ink
*/
const createDynamicLog = ({
stream = process.stdout,
clearTerminalAllowed,
onVerticalOverflow = () => {},
onWriteFromOutside = () => {},
} = {}) => {
const { columns = 80, rows = 24 } = stream;
const dynamicLog = {
destroyed: false,
onVerticalOverflow,
onWriteFromOutside,
};
let lastOutput = "";
let lastOutputFromOutside = "";
let clearAttemptResult;
let writing = false;
const getErasePreviousOutput = () => {
// nothing to clear
if (!lastOutput) {
return "";
}
if (clearAttemptResult !== undefined) {
return "";
}
const logLines = lastOutput.split(/\r\n|\r|\n/);
let visualLineCount = 0;
for (const logLine of logLines) {
const width = measureTextWidth(logLine);
if (width === 0) {
visualLineCount++;
} else {
visualLineCount += Math.ceil(width / columns);
}
}
if (visualLineCount > rows) {
if (clearTerminalAllowed) {
clearAttemptResult = true;
return clearTerminal;
}
// the whole log cannot be cleared because it's vertically to long
// (longer than terminal height)
// readline.moveCursor cannot move cursor higher than screen height
// it means we would only clear the visible part of the log
// better keep the log untouched
clearAttemptResult = false;
dynamicLog.onVerticalOverflow();
return "";
}
clearAttemptResult = true;
return eraseLines(visualLineCount);
};
const update = (string) => {
if (dynamicLog.destroyed) {
throw new Error("Cannot write log after destroy");
}
let stringToWrite = string;
if (lastOutput) {
if (lastOutputFromOutside) {
// We don't want to clear logs written by other code,
// it makes output unreadable and might erase precious information
// To detect this we put a spy on the stream.
// The spy is required only if we actually wrote something in the stream
// something else than this code has written in the stream
// so we just write without clearing (append instead of replacing)
lastOutput = "";
lastOutputFromOutside = "";
} else {
stringToWrite = `${getErasePreviousOutput()}${string}`;
}
}
writing = true;
stream.write(stringToWrite);
lastOutput = string;
writing = false;
clearAttemptResult = undefined;
};
const clearDuringFunctionCall = (
callback,
ouputAfterCallback = lastOutput,
) => {
// 1. Erase the current log
// 2. Call callback (expect to write something on stdout)
// 3. Restore the current log
// During step 2. we expect a "write from outside" so we uninstall
// the stream spy during function call
update("");
writing = true;
callback(update);
lastOutput = "";
writing = false;
update(ouputAfterCallback);
};
const writeFromOutsideEffect = (value) => {
if (!lastOutput) {
// we don't care if the log never wrote anything
// or if last update() wrote an empty string
return;
}
if (writing) {
return;
}
lastOutputFromOutside = value;
dynamicLog.onWriteFromOutside(value);
};
let removeStreamSpy;
if (stream === process.stdout) {
const removeStdoutSpy = spyStreamOutput(
process.stdout,
writeFromOutsideEffect,
);
const removeStderrSpy = spyStreamOutput(
process.stderr,
writeFromOutsideEffect,
);
removeStreamSpy = () => {
removeStdoutSpy();
removeStderrSpy();
};
} else {
removeStreamSpy = spyStreamOutput(stream, writeFromOutsideEffect);
}
const destroy = () => {
dynamicLog.destroyed = true;
if (removeStreamSpy) {
removeStreamSpy();
removeStreamSpy = null;
lastOutput = "";
lastOutputFromOutside = "";
}
};
Object.assign(dynamicLog, {
update,
destroy,
stream,
clearDuringFunctionCall,
});
return dynamicLog;
};
// maybe https://github.com/gajus/output-interceptor/tree/v3.0.0 ?
// the problem with listening data on stdout
// is that node.js will later throw error if stream gets closed
// while something listening data on it
const spyStreamOutput = (stream, callback) => {
let output = "";
let installed = true;
const originalWrite = stream.write;
stream.write = function (...args /* chunk, encoding, callback */) {
output += args;
callback(output);
return originalWrite.call(this, ...args);
};
const uninstall = () => {
if (!installed) {
return;
}
stream.write = originalWrite;
installed = false;
};
return () => {
uninstall();
return output;
};
};
const startSpinner = ({
dynamicLog,
frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
fps = 20,
keepProcessAlive = false,
stopOnWriteFromOutside = true,
stopOnVerticalOverflow = true,
render = () => "",
effect = () => {},
animated = dynamicLog.stream.isTTY,
}) => {
let frameIndex = 0;
let interval;
let running = true;
const spinner = {
message: undefined,
};
const update = (message) => {
spinner.message = running
? `${frames[frameIndex]} ${message}\n`
: `${message}\n`;
return spinner.message;
};
spinner.update = update;
let cleanup;
if (animated && ANSI.supported) {
running = true;
cleanup = effect();
dynamicLog.update(update(render()));
interval = setInterval(() => {
frameIndex = frameIndex === frames.length - 1 ? 0 : frameIndex + 1;
dynamicLog.update(update(render()));
}, 1000 / fps);
if (!keepProcessAlive) {
interval.unref();
}
} else {
dynamicLog.update(update(render()));
}
const stop = (message) => {
running = false;
if (interval) {
clearInterval(interval);
interval = null;
}
if (cleanup) {
cleanup();
cleanup = null;
}
if (dynamicLog && message) {
dynamicLog.update(update(message));
dynamicLog = null;
}
};
spinner.stop = stop;
if (stopOnVerticalOverflow) {
dynamicLog.onVerticalOverflow = stop;
}
if (stopOnWriteFromOutside) {
dynamicLog.onWriteFromOutside = stop;
}
return spinner;
};
const createTaskLog = (
label,
{ disabled = false, animated = true, stopOnWriteFromOutside } = {},
) => {
if (disabled) {
return {
setRightText: () => {},
done: () => {},
happen: () => {},
fail: () => {},
};
}
if (animated && process.env.CAPTURING_SIDE_EFFECTS) {
animated = false;
}
const startMs = Date.now();
const dynamicLog = createDynamicLog();
let message = label;
const taskSpinner = startSpinner({
dynamicLog,
render: () => message,
stopOnWriteFromOutside,
animated,
});
return {
setRightText: (value) => {
message = `${label} ${value}`;
},
done: () => {
const msEllapsed = Date.now() - startMs;
taskSpinner.stop(
`${UNICODE.OK} ${label} (done in ${humanizeDuration(msEllapsed)})`,
);
},
happen: (message) => {
taskSpinner.stop(
`${UNICODE.INFO} ${message} (at ${new Date().toLocaleTimeString()})`,
);
},
fail: (message = `failed to ${label}`) => {
taskSpinner.stop(`${UNICODE.FAILURE} ${message}`);
},
};
};
// consider switching to https://babeljs.io/docs/en/babel-code-frame
// https://github.com/postcss/postcss/blob/fd30d3df5abc0954a0ec642a3cdc644ab2aacf9c/lib/css-syntax-error.js#L43
// https://github.com/postcss/postcss/blob/fd30d3df5abc0954a0ec642a3cdc644ab2aacf9c/lib/terminal-highlight.js#L50
// https://github.com/babel/babel/blob/eea156b2cb8deecfcf82d52aa1b71ba4995c7d68/packages/babel-code-frame/src/index.js#L1
const stringifyUrlSite = (
{ url, line, column, content },
{ showCodeFrame = true, ...params } = {},
) => {
let string = url;
if (typeof line === "number") {
string += `:${line}`;
if (typeof column === "number") {
string += `:${column}`;
}
}
if (!showCodeFrame || typeof line !== "number" || !content) {
return string;
}
const sourceLoc = generateContentFrame({
content,
line,
column});
return `${string}
${sourceLoc}`;
};
const pathnameToExtension$1 = (pathname) => {
const slashLastIndex = pathname.lastIndexOf("/");
const filename =
slashLastIndex === -1 ? pathname : pathname.slice(slashLastIndex + 1);
if (filename.match(/@([0-9])+(\.[0-9]+)?(\.[0-9]+)?$/)) {
return "";
}
const dotLastIndex = filename.lastIndexOf(".");
if (dotLastIndex === -1) {
return "";
}
// if (dotLastIndex === pathname.length - 1) return ""
const extension = filename.slice(dotLastIndex);
return extension;
};
const resourceToPathname = (resource) => {
const searchSeparatorIndex = resource.indexOf("?");
if (searchSeparatorIndex > -1) {
return resource.slice(0, searchSeparatorIndex);
}
const hashIndex = resource.indexOf("#");
if (hashIndex > -1) {
return resource.slice(0, hashIndex);
}
return resource;
};
const urlToScheme$1 = (url) => {
const urlString = String(url);
const colonIndex = urlString.indexOf(":");
if (colonIndex === -1) {
return "";
}
const scheme = urlString.slice(0, colonIndex);
return scheme;
};
const urlToResource = (url) => {
const scheme = urlToScheme$1(url);
if (scheme === "file") {
const urlAsStringWithoutFileProtocol = String(url).slice("file://".length);
return urlAsStringWithoutFileProtocol;
}
if (scheme === "https" || scheme === "http") {
// remove origin
const afterProtocol = String(url).slice(scheme.length + "://".length);
const