@jsenv/core
Version:
Tool to develop, test and build js projects
1,916 lines (1,734 loc) • 166 kB
JavaScript
import { createSupportsColor, isUnicodeSupported, emojiRegex, eastAsianWidth, clearTerminal, eraseLines } from "./jsenv_core_node_modules.js";
import { stripVTControlCharacters } from "node:util";
import { existsSync, readFileSync, chmodSync, statSync, lstatSync, readdirSync, openSync, closeSync, unlinkSync, rmdirSync, mkdirSync, writeFileSync as writeFileSync$1, watch, realpathSync } from "node:fs";
import { extname } from "node:path";
import crypto, { createHash } from "node:crypto";
import { pathToFileURL, fileURLToPath } from "node:url";
/*
* 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 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 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 formatError = (error) => {
let text = "";
text += error.stack;
const { cause } = error;
if (cause) {
const formatCause = (cause, depth) => {
let causeText = prefixFirstAndIndentRemainingLines({
prefix: " [cause]:",
indentation: " ".repeat(depth + 1),
text: cause.stack,
});
const nestedCause = cause.cause;
if (nestedCause) {
const nestedCauseText = formatCause(nestedCause, depth + 1);
causeText += `\n${nestedCauseText}`;
}
return causeText;
};
const causeText = formatCause(cause, 0);
text += `\n${causeText}`;
}
return text;
};
const prefixFirstAndIndentRemainingLines = ({
prefix,
indentation,
text,
trimLines,
trimLastLine,
}) => {
const lines = text.split(/\r?\n/);
const firstLine = lines.shift();
if (indentation === undefined) {
{
indentation = " "; // prefix + space
}
}
let result = `${prefix} ${firstLine}` ;
let i = 0;
while (i < lines.length) {
const line = trimLines ? lines[i].trim() : lines[i];
i++;
result += line.length
? `\n${indentation}${line}`
: trimLastLine && i === lines.length
? ""
: `\n`;
}
return result;
};
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 pathnameSlashIndex = afterProtocol.indexOf("/", "://".length);
const urlAsStringWithoutOrigin = afterProtocol.slice(pathnameSlashIndex);
return urlAsStringWithoutOrigin;
}
const urlAsStringWithoutProtocol = String(url).slice(scheme.length + 1);
return urlAsStringWithoutProtocol;
};
const urlToPathname$1 = (url) => {
const resource = urlToResource(url);
const pathname = resourceToPathname(resource);
return pathname;
};
const urlToFilename$1 = (url) => {
const pathname = urlToPathname$1(url);
return pathnameToFilename(pathname);
};
const pathnameToFilename = (pathname) => {
const pathnameBeforeLastSlash = pathname.endsWith("/")
? pathname.slice(0, -1)
: pathname;
const slashLastIndex = pathnameBeforeLastSlash.lastIndexOf("/");
const filename =
slashLastIndex === -1
? pathnameBeforeLastSlash
: pathnameBeforeLastSlash.slice(slashLastIndex + 1);
return filename;
};
const urlToBasename = (url, removeAllExtensions) => {
const filename = urlToFilename$1(url);
const basename = filenameToBasename(filename);
{
return basename;
}
};
const filenameToBasename = (filename) => {
const dotLastIndex = filename.lastIndexOf(".");
const basename =
dotLastIndex === -1 ? filename : filename.slice(0, dotLastIndex);
return basename;
};
const urlToExtension$1 = (url) => {
const pathname = urlToPathname$1(url);
return pathnameToExtension$1(pathname);
};
const setUrlExtension = (
url,
extension,
{ trailingSlash = "preserve" } = {},
) => {
return transformUrlPathname(url, (pathname) => {
const currentExtension = urlToExtension$1(url);
if (typeof extension === "function") {
extension = extension(currentExtension);
}
const pathnameWithoutExtension = currentExtension
? pathname.slice(0, -currentExtension.length)
: pathname;
if (pathnameWithoutExtension.endsWith("/")) {
let pathnameWithExtension;
pathnameWithExtension = pathnameWithoutExtension.slice(0, -1);
pathnameWithExtension += extension;
if (trailingSlash === "preserve") {
pathnameWithExtension += "/";
}
return pathnameWithExtension;
}
let pathnameWithExtension = pathnameWithoutExtension;
pathnameWithExtension += extension;
return pathnameWithExtension;
});
};
const setUrlFilename = (url, filename) => {
const parentPathname = new URL("./", url).pathname;
return transformUrlPathname(url, (pathname) => {
if (typeof filename === "function") {
filename = filename(pathnameToFilename(pathname));
}
return `${parentPathname}${filename}`;
});
};
const setUrlBasename = (url, basename) => {
return setUrlFilename(url, (filename) => {
if (typeof basename === "function") {
basename = basename(filenameToBasename(filename));
}
return `${basename}${urlToExtension$1(url)}`;
});
};
const transformUrlPathname = (url, transformer) => {
if (typeof url === "string") {
const urlObject = new URL(url);
const { pathname } = urlObject;
const pathnameTransformed = transformer(pathname);
if (pathnameTransformed === pathname) {
return url;
}
let { origin } = urlObject;
// origin is "null" for "file://" urls with Node.js
if (origin === "null" && urlObject.href.startsWith("file:")) {
origin = "file://";
}
const { search, hash } = urlObject;
const urlWithPathnameTransformed = `${origin}${pathnameTransformed}${search}${hash}`;
return urlWithPathnameTransformed;
}
const pathnameTransformed = transformer(url.pathname);
url.pathname = pathnameTransformed;
return url;
};
const ensurePathnameTrailingSlash = (url) => {
return transformUrlPathname(url, (pathname) => {
return pathname.endsWith("/") ? pathname : `${pathname}/`;
});
};
const asUrlWithoutSearch = (url) => {
url = String(url);
if (url.includes("?")) {
const urlObject = new URL(url);
urlObject.search = "";
return urlObject.href;
}
return url;
};
const isValidUrl$1 = (url) => {
try {
// eslint-disable-next-line no-new
new URL(url);
return true;
} catch {
return false;
}
};
const asSpecifierWithoutSearch = (specifier) => {
if (isValidUrl$1(specifier)) {
return asUrlWithoutSearch(specifier);
}
const [beforeQuestion] = specifier.split("?");
return beforeQuestion;
};
// normalize url search params:
// Using URLSearchParams to alter the url search params
// can result into "file:///file.css?css_module"
// becoming "file:///file.css?css_module="
// we want to get rid of the "=" and consider it's the same url
const normalizeUrl = (url) => {
const calledWithString = typeof url === "string";
const urlObject = calledWithString ? new URL(url) : url;
let urlString = urlObject.href;
if (!urlString.includes("?")) {
return url;
}
// disable on data urls (would mess up base64 encoding)
if (urlString.startsWith("data:")) {
return url;
}
urlString = urlString.replace(/[=](?=&|$)/g, "");
if (calledWithString) {
return urlString;
}
urlObject.href = urlString;
return urlObject;
};
const injectQueryParamsIntoSpecifier = (specifier, params) => {
if (isValidUrl$1(specifier)) {
return injectQueryParams(specifier, params);
}
const [beforeQuestion, afterQuestion = ""] = specifier.split("?");
const searchParams = new URLSearchParams(afterQuestion);
Object.keys(params).forEach((key) => {
const value = params[key];
if (value === undefined) {
searchParams.delete(key);
} else {
searchParams.set(key, value);
}
});
let paramsString = searchParams.toString();
if (paramsString) {
paramsString = paramsString.replace(/[=](?=&|$)/g, "");
return `${beforeQuestion}?${paramsString}`;
}
return beforeQuestion;
};
const injectQueryParams = (url, params) => {
const calledWithString = typeof url === "string";
const urlObject = calledWithString ? new URL(url) : url;
const { searchParams } = urlObject;
for (const key of Object.keys(params)) {
const value = params[key];
if (value === undefined) {
searchParams.delete(key);
} else {
searchParams.set(key, value);
}
}
return normalizeUrl(calledWithString ? urlObject.href : urlObject);
};
const getCommonPathname = (pathname, otherPathname) => {
if (pathname === otherPathname) {
return pathname;
}
let commonPart = "";
let commonPathname = "";
let i = 0;
const length = pathname.length;
const otherLength = otherPathname.length;
while (i < length) {
const char = pathname.charAt(i);
const otherChar = otherPathname.charAt(i);
i++;
if (char === otherChar) {
if (char === "/") {
commonPart += "/";
commonPathname += commonPart;
commonPart = "";
} else {
commonPart += char;
}
} else {
if (char === "/" && i - 1 === otherLength) {
commonPart += "/";
commonPathname += commonPart;
}
return commonPathname;
}
}
if (length === otherLength) {
commonPathname += commonPart;
} else if (otherPathname.charAt(i) === "/") {
commonPathname += commonPart;
}
return commonPathname;
};
const urlToRelativeUrl = (
url,
baseUrl,
{ preferRelativeNotation } = {},
) => {
const urlObject = new URL(url);
const baseUrlObject = new URL(baseUrl);
if (urlObject.protocol !== baseUrlObject.protocol) {
const urlAsString = String(url);
return urlAsString;
}
if (
urlObject.username !== baseUrlObject.username ||
urlObject.password !== baseUrlObject.password ||
urlObject.host !== baseUrlObject.host
) {
const afterUrlScheme = String(url).slice(urlObject.protocol.length);
return afterUrlScheme;
}
const { pathname, hash, search } = urlObject;
if (pathname === "/") {
const baseUrlResourceWithoutLeadingSlash = baseUrlObject.pathname.slice(1);
return baseUrlResourceWithoutLeadingSlash;
}
const basePathname = baseUrlObject.pathname;
const commonPathname = getCommonPathname(pathname, basePathname);
if (!commonPathname) {
const urlAsString = String(url);
return urlAsString;
}
const specificPathname = pathname.slice(commonPathname.length);
const baseSpecificPathname = basePathname.slice(commonPathname.length);
if (baseSpecificPathname.includes("/")) {
const baseSpecificParentPathname =
pathnameToParentPathname$1(baseSpecificPathname);
const relativeDirectoriesNotation = baseSpecificParentPathname.replace(
/.*?\//g,
"../",
);
const relativeUrl = `${relativeDirectoriesNotation}${specificPathname}${search}${hash}`;
return relativeUrl;
}
const relativeUrl = `${specificPathname}${search}${hash}`;
return preferRelativeNotation ? `./${relativeUrl}` : relativeUrl;
};
const pathnameToParentPathname$1 = (pathname) => {
const slashLastIndex = pathname.lastIndexOf("/");
if (slashLastIndex === -1) {
return "/";
}
return pathname.slice(0, slashLastIndex + 1);
};
const moveUrl = ({ url, from, to, preferRelative }) => {
let relativeUrl = urlToRelativeUrl(url, from);
if (relativeUrl.slice(0, 2) === "//") {
// restore the protocol
relativeUrl = new URL(relativeUrl, url).href;
}
const absoluteUrl = new URL(relativeUrl, to).href;
if (preferRelative) {
return urlToRelativeUrl(absoluteUrl, to);
}
return absoluteUrl;
};
const isFileSystemPath = (value) => {
if (typeof value !== "string") {
throw new TypeError(
`isFileSystemPath first arg must be a string, got ${value}`,
);
}
if (value[0] === "/") {
return true;
}
return startsWithWindowsDriveLetter(value);
};
const startsWithWindowsDriveLetter = (string) => {
const firstChar = string[0];
if (!/[a-zA-Z]/.test(firstChar)) return false;
const secondChar = string[1];
if (secondChar !== ":") return false;
return true;
};
const resolveUrl$1 = (specifier, baseUrl) => {
if (typeof baseUrl === "undefined") {
throw new TypeError(`baseUrl missing to resolve ${specifier}`);
}
return String(new URL(specifier, baseUrl));
};
const urlIsOrIsInsideOf = (url, otherUrl) => {
const urlObject = new URL(url);
const otherUrlObject = new URL(otherUrl);
if (urlObject.origin !== otherUrlObject.origin) {
return false;
}
const urlPathname = urlObject.pathname;
const otherUrlPathname = otherUrlObject.pathname;
if (urlPathname === otherUrlPathname) {
return true;
}
const isInside = urlPathname.startsWith(otherUrlPathname);
return isInside;
};
const fileSystemPathToUrl = (value) => {
if (!isFileSystemPath(value)) {
throw new Error(`value must be a filesystem path, got ${value}`);
}
return String(pathToFileURL(value));
};
const getCallerPosition = () => {
const { prepareStackTrace } = Error;
Error.prepareStackTrace = (error, stack) => {
Error.prepareStackTrace = prepareStackTrace;
return stack;
};
const { stack } = new Error();
const callerCallsite = stack[2];
const fileName = callerCallsite.getFileName();
return {
url:
fileName && isFileSystemPath(fileName)
? fileSystemPathToUrl(fileName)
: fileName,
line: callerCallsite.getLineNumber(),
column: callerCallsite.getColumnNumber(),
};
};
const urlToFileSystemPath = (url) => {
const urlObject = new URL(url);
let { origin, pathname, hash } = urlObject;
if (urlObject.protocol === "file:") {
origin = "file://";
}
pathname = pathname
.split("/")
.map((part) => {
return part.replace(/%(?![0-9A-F][0-9A-F])/g, "%25");
})
.join("/");
if (hash) {
pathname += `%23${encodeURIComponent(hash.slice(1))}`;
}
const urlString = `${origin}${pathname}`;
const fileSystemPath = fileURLToPath(urlString);
if (fileSystemPath[fileSystemPath.length - 1] === "/") {
// remove trailing / so that nodejs path becomes predictable otherwise it logs
// the trailing slash on linux but does not on windows
return fileSystemPath.slice(0, -1);
}
return fileSystemPath;
};
const validateDirectoryUrl = (value) => {
let urlString;
if (value instanceof URL) {
urlString = value.href;
} else if (typeof value === "string") {
if (isFileSystemPath(value)) {
urlString = fileSystemPathToUrl(value);
} else {
try {
urlString = String(new URL(value));
} catch {
return {
valid: false,
value,
message: `must be a valid url`,
};
}
}
} else if (
value &&
typeof value === "object" &&
typeof value.href === "string"
) {
value = value.href;
} else {
return {
valid: false,
value,
message: `must be a string or an url`,
};
}
if (!urlString.startsWith("file://")) {
return {
valid: false,
value,
message: 'must start with "file://"',
};
}
return {
valid: true,
value: ensurePathnameTrailingSlash(urlString),
};
};
const assertAndNormalizeDirectoryUrl = (
directoryUrl,
name = "directoryUrl",
) => {
const { valid, message, value } = validateDirectoryUrl(directoryUrl);
if (!valid) {
throw new TypeError(`${name} ${message}, got ${value}`);
}
return value;
};
const validateFileUrl = (value, baseUrl) => {
let urlString;
if (value instanceof URL) {
urlString = value.href;
} else if (typeof value === "string") {
if (isFileSystemPath(value)) {
urlString = fileSystemPathToUrl(value);
} else {
try {
urlString = String(new URL(value, baseUrl));
} catch {
return {
valid: false,
value,
message: "must be a valid url",
};
}
}
} else {
return {
valid: false,
value,
message: "must be a string or an url",
};
}
if (!urlString.startsWith("file://")) {
return {
valid: false,
value,
message: 'must start with "file://"',
};
}
return {
valid: true,
value: urlString,
};
};
const assertAndNormalizeFileUrl = (
fileUrl,
baseUrl,
name = "fileUrl",
) => {
const { valid, message, value } = validateFileUrl(fileUrl, baseUrl);
if (!valid) {
throw new TypeError(`${name} ${message}, got ${fileUrl}`);
}
return value;
};
const comparePathnames = (leftPathame, rightPathname) => {
const leftPartArray = leftPathame.split("/");
const rightPartArray = rightPathname.split("/");
const leftLength = leftPartArray.length;
const rightLength = rightPartArray.length;
const maxLength = Math.max(leftLength, rightLength);
let i = 0;
while (i < maxLength) {
const leftPartExists = i in leftPartArray;
const rightPartExists = i in rightPartArray;
// longer comes first
if (!leftPartExists) {
return 1;
}
if (!rightPartExists) {
return -1;
}
const leftPartIsLast = i === leftPartArray.length - 1;
const rightPartIsLast = i === rightPartArray.length - 1;
// folder comes first
if (leftPartIsLast && !rightPartIsLast) {
return 1;
}
if (!leftPartIsLast && rightPartIsLast) {
return -1;
}
const leftPart = leftPartArray[i];
const rightPart = rightPartArray[i];
i++;
// local comparison comes first
const comparison = leftPart.localeCompare(rightPart);
if (comparison !== 0) {
return comparison;
}
}
if (leftLength < rightLength) {
return 1;
}
if (leftLength > rightLength) {
return -1;
}
return 0;
};
const compareFileUrls = (a, b) => {
return comparePathnames(new URL(a).pathname, new URL(b).pathname);
};
const isWindows$2 = process.platform === "win32";
const baseUrlFallback = fileSystemPathToUrl(process.cwd());
/**
* Some url might be resolved or remapped to url without the windows drive letter.
* For instance
* new URL('/foo.js', 'file:///C:/dir/file.js')
* resolves to
* 'file:///foo.js'
*
* But on windows it becomes a problem because we need the drive letter otherwise
* url cannot be converted to a filesystem path.
*
* ensureWindowsDriveLetter ensure a resolved url still contains the drive letter.
*/
const ensureWindowsDriveLetter = (url, baseUrl) => {
try {
url = String(new URL(url));
} catch {
throw new Error(`absolute url expect but got ${url}`);
}
if (!isWindows$2) {
return url;
}
try {
baseUrl = String(new URL(baseUrl));
} catch {
throw new Error(
`absolute baseUrl expect but got ${baseUrl} to ensure windows drive letter on ${url}`,
);
}
if (!url.startsWith("file://")) {
return url;
}
const afterProtocol = url.slice("file://".length);
// we still have the windows drive letter
if (extractDriveLetter(afterProtocol)) {
return url;
}
// drive letter was lost, restore it
const baseUrlOrFallback = baseUrl.startsWith("file://")
? baseUrl
: baseUrlFallback;
const driveLetter = extractDriveLetter(
baseUrlOrFallback.slice("file://".length),
);
if (!driveLetter) {
throw new Error(
`drive letter expect on baseUrl but got ${baseUrl} to ensure windows drive letter on ${url}`,
);
}
return `file:///${driveLetter}:${afterProtocol}`;
};
const extractDriveLetter = (resource) => {
// we still have the windows drive letter
if (/[a-zA-Z]/.test(resource[1]) && resource[2] === ":") {
return resource[1];
}
return null;
};
const getParentDirectoryUrl = (url) => {
if (url.startsWith("file://")) {
// With node.js new URL('../', 'file:///C:/').href
// returns "file:///C:/" instead of "file:///"
const resource = url.slice("file://".length);
const