UNPKG

@jsenv/core

Version:

Tool to develop, test and build js projects

1,890 lines (1,704 loc) 306 kB
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, "&amp;") .replace(/</g, "&lt;") .replace(/>/g, "&gt;") .replace(/"/g, "&quot;") .replace(/'/g, "&#039;"); }; 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