UNPKG

@jsenv/core

Version:

Tool to develop, test and build js projects

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