diff options
Diffstat (limited to 'src/util')
-rw-r--r-- | src/util/aggregate.js | 728 | ||||
-rw-r--r-- | src/util/cli.js | 521 |
2 files changed, 0 insertions, 1249 deletions
diff --git a/src/util/aggregate.js b/src/util/aggregate.js deleted file mode 100644 index c7648c4c..00000000 --- a/src/util/aggregate.js +++ /dev/null @@ -1,728 +0,0 @@ -import {colors} from './cli.js'; -import {empty, typeAppearance} from './sugar.js'; - -// Utility function for providing useful interfaces to the JS AggregateError -// class. -// -// Generally, this works by returning a set of interfaces which operate on -// functions: wrap() takes a function and returns a new function which passes -// its arguments through and appends any resulting error to the internal error -// list; call() simplifies this process by wrapping the provided function and -// then calling it immediately. Once the process for which errors should be -// aggregated is complete, close() constructs and throws an AggregateError -// object containing all caught errors (or doesn't throw anything if there were -// no errors). -export function openAggregate({ - // Constructor to use, defaulting to the builtin AggregateError class. - // Anything passed here should probably extend from that! May be used for - // letting callers programatically distinguish between multiple aggregate - // errors. - // - // This should be provided using the aggregateThrows utility function. - [openAggregate.errorClassSymbol]: errorClass = AggregateError, - - // Optional human-readable message to describe the aggregate error, if - // constructed. - message = '', - - // Optional flag to indicate that this layer of the aggregate error isn't - // generally useful outside of developer debugging purposes - it will be - // skipped by default when using showAggregate, showing contained errors - // inline with other children of this aggregate's parent. - // - // If set to 'single', it'll be hidden only if there's a single error in the - // aggregate (so it's not grouping multiple errors together). - translucent = false, - - // Value to return when a provided function throws an error. If this is a - // function, it will be called with the arguments given to the function. - // (This is primarily useful when wrapping a function and then providing it - // to another utility, e.g. array.map().) - returnOnFail = null, -} = {}) { - const errors = []; - - const aggregate = {}; - - aggregate.wrap = - (fn) => - (...args) => { - try { - return fn(...args); - } catch (error) { - errors.push(error); - return typeof returnOnFail === 'function' - ? returnOnFail(...args) - : returnOnFail; - } - }; - - aggregate.wrapAsync = - (fn) => - (...args) => { - return fn(...args).then( - (value) => value, - (error) => { - errors.push(error); - return typeof returnOnFail === 'function' - ? returnOnFail(...args) - : returnOnFail; - } - ); - }; - - aggregate.push = (error) => { - errors.push(error); - }; - - aggregate.call = (fn, ...args) => { - return aggregate.wrap(fn)(...args); - }; - - aggregate.callAsync = (fn, ...args) => { - return aggregate.wrapAsync(fn)(...args); - }; - - aggregate.nest = (...args) => { - return aggregate.call(() => withAggregate(...args)); - }; - - aggregate.nestAsync = (...args) => { - return aggregate.callAsync(() => withAggregateAsync(...args)); - }; - - aggregate.receive = (results) => { - if (!Array.isArray(results)) { - if (typeof results === 'object' && results.aggregate) { - const {aggregate, result} = results; - - try { - aggregate.close(); - } catch (error) { - errors.push(error); - } - - return result; - } - - throw new Error(`Expected an array or {aggregate, result} object`); - } - - return results.map(({aggregate, result}) => { - if (!aggregate) { - throw new Error(`Expected an array of {aggregate, result} objects`); - } - - try { - aggregate.close(); - } catch (error) { - errors.push(error); - } - - return result; - }); - }; - - aggregate.contain = (results) => { - return { - aggregate, - result: aggregate.receive(results), - }; - }; - - aggregate.map = (...args) => { - const parent = aggregate; - const {result, aggregate: child} = mapAggregate(...args); - parent.call(child.close); - return result; - }; - - aggregate.mapAsync = async (...args) => { - const parent = aggregate; - const {result, aggregate: child} = await mapAggregateAsync(...args); - parent.call(child.close); - return result; - }; - - aggregate.filter = (...args) => { - const parent = aggregate; - const {result, aggregate: child} = filterAggregate(...args); - parent.call(child.close); - return result; - }; - - aggregate.throws = aggregateThrows; - - aggregate.close = () => { - if (errors.length) { - const error = Reflect.construct(errorClass, [errors, message]); - - if (translucent) { - error[Symbol.for('hsmusic.aggregate.translucent')] = translucent; - } - - throw error; - } - }; - - return aggregate; -} - -openAggregate.errorClassSymbol = Symbol('error class'); - -// Utility function for providing {errorClass} parameter to aggregate functions. -export function aggregateThrows(errorClass) { - return {[openAggregate.errorClassSymbol]: errorClass}; -} - -// Helper function for allowing both (fn, opts) and (opts, fn) in aggregate -// utilities (or other shapes besides functions). -function _reorganizeAggregateArguments(arg1, arg2, desire = v => typeof v === 'function') { - if (desire(arg1)) { - return [arg1, arg2 ?? {}]; - } else if (desire(arg2)) { - return [arg2, arg1]; - } else { - return [undefined, undefined]; - } -} - -// Takes a list of {aggregate, result} objects, puts all the aggregates into -// a new aggregate, and puts all the results into an array, returning both on -// a new {aggregate, result} object. This is essentailly the generalized -// composable version of functions like mapAggregate or filterAggregate. -export function receiveAggregate(arg1, arg2) { - const [array, opts] = _reorganizeAggregateArguments(arg1, arg2, Array.isArray); - if (!array) { - throw new Error(`Expected an array`); - } - - const aggregate = openAggregate(opts); - const result = aggregate.receive(array); - return {aggregate, result}; -} - -// Performs an ordinary array map with the given function, collating into a -// results array (with errored inputs filtered out) and an error aggregate. -// -// Optionally, override returnOnFail to disable filtering and map errored inputs -// to a particular output. -// -// Note the aggregate property is the result of openAggregate(), still unclosed; -// use aggregate.close() to throw the error. (This aggregate may be passed to a -// parent aggregate: `parent.call(aggregate.close)`!) -export function mapAggregate(array, arg1, arg2) { - const [fn, opts] = _reorganizeAggregateArguments(arg1, arg2); - if (!fn) { - throw new Error(`Expected a function`); - } - - return _mapAggregate('sync', null, array, fn, opts); -} - -export function mapAggregateAsync(array, arg1, arg2) { - const [fn, opts] = _reorganizeAggregateArguments(arg1, arg2); - if (!fn) { - throw new Error(`Expected a function`); - } - - const {promiseAll = Promise.all.bind(Promise), ...remainingOpts} = opts; - return _mapAggregate('async', promiseAll, array, fn, remainingOpts); -} - -// Helper function for mapAggregate which holds code common between sync and -// async versions. -export function _mapAggregate(mode, promiseAll, array, fn, aggregateOpts) { - const failureSymbol = Symbol(); - - const aggregate = openAggregate({ - returnOnFail: failureSymbol, - ...aggregateOpts, - }); - - if (mode === 'sync') { - const result = array - .map(aggregate.wrap(fn)) - .filter((value) => value !== failureSymbol); - return {result, aggregate}; - } else { - return promiseAll(array.map(aggregate.wrapAsync(fn))) - .then((values) => { - const result = values.filter((value) => value !== failureSymbol); - return {result, aggregate}; - }); - } -} - -// Performs an ordinary array filter with the given function, collating into a -// results array (with errored inputs filtered out) and an error aggregate. -// -// Optionally, override returnOnFail to disable filtering errors and map errored -// inputs to a particular output. -// -// As with mapAggregate, the returned aggregate property is not yet closed. -export function filterAggregate(array, arg1, arg2) { - const [fn, opts] = _reorganizeAggregateArguments(arg1, arg2); - if (!fn) { - throw new Error(`Expected a function`); - } - - return _filterAggregate('sync', null, array, fn, opts); -} - -export async function filterAggregateAsync(array, arg1, arg2) { - const [fn, opts] = _reorganizeAggregateArguments(arg1, arg2); - if (!fn) { - throw new Error(`Expected a function`); - } - - const {promiseAll = Promise.all.bind(Promise), ...remainingOpts} = opts; - return _filterAggregate('async', promiseAll, array, fn, remainingOpts); -} - -// Helper function for filterAggregate which holds code common between sync and -// async versions. -function _filterAggregate(mode, promiseAll, array, fn, aggregateOpts) { - const failureSymbol = Symbol(); - - const aggregate = openAggregate({ - returnOnFail: failureSymbol, - ...aggregateOpts, - }); - - function filterFunction(value) { - // Filter out results which match the failureSymbol, i.e. errored - // inputs. - if (value === failureSymbol) return false; - - // Always keep results which match the overridden returnOnFail - // value, if provided. - if (value === aggregateOpts.returnOnFail) return true; - - // Otherwise, filter according to the returned value of the wrapped - // function. - return value.output; - } - - function mapFunction(value) { - // Then turn the results back into their corresponding input, or, if - // provided, the overridden returnOnFail value. - return value === aggregateOpts.returnOnFail ? value : value.input; - } - - if (mode === 'sync') { - const result = array - .map(aggregate.wrap((input, index, array) => { - const output = fn(input, index, array); - return {input, output}; - })) - .filter(filterFunction) - .map(mapFunction); - - return {result, aggregate}; - } else { - return promiseAll( - array.map(aggregate.wrapAsync(async (input, index, array) => { - const output = await fn(input, index, array); - return {input, output}; - })) - ).then((values) => { - const result = values.filter(filterFunction).map(mapFunction); - - return {result, aggregate}; - }); - } -} - -// Totally sugar function for opening an aggregate, running the provided -// function with it, then closing the function and returning the result (if -// there's no throw). -export function withAggregate(arg1, arg2) { - const [fn, opts] = _reorganizeAggregateArguments(arg1, arg2); - if (!fn) { - throw new Error(`Expected a function`); - } - - return _withAggregate('sync', opts, fn); -} - -export function withAggregateAsync(arg1, arg2) { - const [fn, opts] = _reorganizeAggregateArguments(arg1, arg2); - if (!fn) { - throw new Error(`Expected a function`); - } - - return _withAggregate('async', opts, fn); -} - -export function _withAggregate(mode, aggregateOpts, fn) { - const aggregate = openAggregate(aggregateOpts); - - if (mode === 'sync') { - const result = fn(aggregate); - aggregate.close(); - return result; - } else { - return fn(aggregate).then((result) => { - aggregate.close(); - return result; - }); - } -} - -export const unhelpfulTraceLines = [ - /sugar/, - /sort/, - /aggregate/, - /composite/, - /cacheable-object/, - /node:/, - /<anonymous>/, -]; - -export function getUsefulTraceLine(trace, {helpful, unhelpful}) { - if (!trace) return ''; - - for (const traceLine of trace.split('\n')) { - if (!traceLine.trim().startsWith('at')) { - continue; - } - - if (!empty(unhelpful)) { - if (unhelpful.some(regex => regex.test(traceLine))) { - continue; - } - } - - if (!empty(helpful)) { - for (const regex of helpful) { - const match = traceLine.match(regex); - - if (match) { - return match[1] ?? traceLine; - } - } - - continue; - } - - return traceLine; - } - - return ''; -} - -export function showAggregate(topError, { - pathToFileURL = f => f, - showTraces = true, - showTranslucent = showTraces, - print = true, -} = {}) { - const getTranslucency = error => - error[Symbol.for('hsmusic.aggregate.translucent')] ?? false; - - const determineCauseHelper = cause => { - if (!cause) { - return null; - } - - const translucency = getTranslucency(cause); - - if (!translucency) { - return cause; - } - - if (translucency === 'single') { - if (cause.errors?.length === 1) { - return determineCauseHelper(cause.errors[0]); - } else { - return cause; - } - } - - return determineCauseHelper(cause.cause); - }; - - const determineCause = error => - (showTranslucent - ? error.cause ?? null - : determineCauseHelper(error.cause)); - - const determineErrorsHelper = error => { - const translucency = getTranslucency(error); - - if (!translucency) { - return [error]; - } - - if (translucency === 'single' && error.errors?.length >= 2) { - return [error]; - } - - const errors = []; - - if (error.cause) { - errors.push(...determineErrorsHelper(error.cause)); - } - - if (error.errors) { - errors.push(...error.errors.flatMap(determineErrorsHelper)); - } - - return errors; - }; - - const determineErrors = error => - (showTranslucent - ? error.errors ?? null - : error.errors?.flatMap(determineErrorsHelper) ?? null); - - const flattenErrorStructure = (error, level = 0) => { - const cause = determineCause(error); - const errors = determineErrors(error); - - return { - level, - - kind: error.constructor.name, - message: error.message, - - trace: - (error[Symbol.for(`hsmusic.aggregate.traceFrom`)] - ? error[Symbol.for(`hsmusic.aggregate.traceFrom`)].stack - : error.stack), - - cause: - (cause - ? flattenErrorStructure(cause, level + 1) - : null), - - errors: - (errors - ? errors.map(error => flattenErrorStructure(error, level + 1)) - : null), - - options: { - alwaysTrace: - error[Symbol.for(`hsmusic.aggregate.alwaysTrace`)], - - helpfulTraceLines: - error[Symbol.for(`hsmusic.aggregate.helpfulTraceLines`)], - - unhelpfulTraceLines: - error[Symbol.for(`hsmusic.aggregate.unhelpfulTraceLines`)], - } - }; - }; - - const recursive = ({ - level, - kind, - message, - trace, - cause, - errors, - options: { - alwaysTrace, - helpfulTraceLines: ownHelpfulTraceLines, - unhelpfulTraceLines: ownUnhelpfulTraceLines, - }, - }, index, apparentSiblings) => { - const subApparentSiblings = - (cause && errors - ? [cause, ...errors] - : cause - ? [cause] - : errors - ? errors - : []); - - const anythingHasErrorsThisLayer = - apparentSiblings.some(({errors}) => !empty(errors)); - - const messagePart = - message || `(no message)`; - - const kindPart = - kind || `unnamed kind`; - - let headerPart = - (showTraces - ? `[${kindPart}] ${messagePart}` - : errors - ? `[${messagePart}]` - : anythingHasErrorsThisLayer - ? ` ${messagePart}` - : messagePart); - - if (showTraces || alwaysTrace) { - const traceLine = - getUsefulTraceLine(trace, { - unhelpful: - (ownUnhelpfulTraceLines - ? unhelpfulTraceLines.concat(ownUnhelpfulTraceLines) - : unhelpfulTraceLines), - - helpful: - (ownHelpfulTraceLines - ? ownHelpfulTraceLines - : null), - }); - - const tracePart = - (traceLine - ? '- ' + - traceLine - .trim() - .replace(/file:\/\/.*\.js/, (match) => pathToFileURL(match)) - : '(no stack trace)'); - - headerPart += ` ${colors.dim(tracePart)}`; - } - - const head1 = level % 2 === 0 ? '\u21aa' : colors.dim('\u21aa'); - const bar1 = ' '; - - const causePart = - (cause - ? recursive(cause, 0, subApparentSiblings) - .split('\n') - .map((line, i) => i === 0 ? ` ${head1} ${line}` : ` ${bar1} ${line}`) - .join('\n') - : ''); - - const head2 = level % 2 === 0 ? '\u257f' : colors.dim('\u257f'); - const bar2 = level % 2 === 0 ? '\u2502' : colors.dim('\u254e'); - - const errorsPart = - (errors - ? errors - .map((error, index) => recursive(error, index + 1, subApparentSiblings)) - .flatMap(str => str.split('\n')) - .map((line, i) => i === 0 ? ` ${head2} ${line}` : ` ${bar2} ${line}`) - .join('\n') - : ''); - - return [headerPart, errorsPart, causePart].filter(Boolean).join('\n'); - }; - - const structure = flattenErrorStructure(topError); - const message = recursive(structure, 0, [structure]); - - if (print) { - console.error(message); - } else { - return message; - } -} - -export function annotateError(error, ...callbacks) { - for (const callback of callbacks) { - error = callback(error) ?? error; - } - - return error; -} - -export function annotateErrorWithIndex(error, index) { - return Object.assign(error, { - [Symbol.for('hsmusic.annotateError.indexInSourceArray')]: - index, - - message: - `(${colors.yellow(`#${index + 1}`)}) ` + - error.message, - }); -} - -export function annotateErrorWithFile(error, file) { - return Object.assign(error, { - [Symbol.for('hsmusic.annotateError.file')]: - file, - - message: - error.message + - (error.message.includes('\n') ? '\n' : ' ') + - `(file: ${colors.bright(colors.blue(file))})`, - }); -} - -export function asyncAdaptiveDecorateError(fn, callback) { - if (typeof callback !== 'function') { - throw new Error(`Expected callback to be a function, got ${typeAppearance(callback)}`); - } - - const syncDecorated = function (...args) { - try { - return fn(...args); - } catch (caughtError) { - throw callback(caughtError, ...args); - } - }; - - const asyncDecorated = async function(...args) { - try { - return await fn(...args); - } catch (caughtError) { - throw callback(caughtError, ...args); - } - }; - - syncDecorated.async = asyncDecorated; - - return syncDecorated; -} - -export function decorateError(fn, callback) { - return asyncAdaptiveDecorateError(fn, callback); -} - -export function asyncDecorateError(fn, callback) { - return asyncAdaptiveDecorateError(fn, callback).async; -} - -export function decorateErrorWithAnnotation(fn, ...annotationCallbacks) { - return asyncAdaptiveDecorateError(fn, - (caughtError, ...args) => - annotateError(caughtError, - ...annotationCallbacks - .map(callback => error => callback(error, ...args)))); -} - -export function decorateErrorWithIndex(fn) { - return decorateErrorWithAnnotation(fn, - (caughtError, _value, index) => - annotateErrorWithIndex(caughtError, index)); -} - -export function decorateErrorWithCause(fn, cause) { - return asyncAdaptiveDecorateError(fn, - (caughtError) => - Object.assign(caughtError, {cause})); -} - -export function asyncDecorateErrorWithAnnotation(fn, ...annotationCallbacks) { - return decorateErrorWithAnnotation(fn, ...annotationCallbacks).async; -} - -export function asyncDecorateErrorWithIndex(fn) { - return decorateErrorWithIndex(fn).async; -} - -export function asyncDecorateErrorWithCause(fn, cause) { - return decorateErrorWithCause(fn, cause).async; -} - -export function conditionallySuppressError(conditionFn, callbackFn) { - return (...args) => { - try { - return callbackFn(...args); - } catch (error) { - if (conditionFn(error, ...args) === true) { - return; - } - - throw error; - } - }; -} diff --git a/src/util/cli.js b/src/util/cli.js deleted file mode 100644 index a40a911f..00000000 --- a/src/util/cli.js +++ /dev/null @@ -1,521 +0,0 @@ -// Utility functions for CLI- and de8ugging-rel8ted stuff. -// -// A 8unch of these depend on process.stdout 8eing availa8le, so they won't -// work within the 8rowser. - -const {process} = globalThis; - -import {sortByName} from './sort.js'; - -export const ENABLE_COLOR = - process && - ((process.env.CLICOLOR_FORCE && process.env.CLICOLOR_FORCE === '1') ?? - (process.env.CLICOLOR && - process.env.CLICOLOR === '1' && - process.stdout.hasColors && - process.stdout.hasColors()) ?? - (process.stdout.hasColors ? process.stdout.hasColors() : true)); - -const C = (n) => - ENABLE_COLOR ? (text) => `\x1b[${n}m${text}\x1b[0m` : (text) => text; - -export const colors = { - bright: C('1'), - dim: C('2'), - normal: C('22'), - black: C('30'), - red: C('31'), - green: C('32'), - yellow: C('33'), - blue: C('34'), - magenta: C('35'), - cyan: C('36'), - white: C('37'), -}; - -const logColor = - (color) => - (literals, ...values) => { - const w = (s) => process.stdout.write(s); - const wc = (text) => { - if (ENABLE_COLOR) w(text); - }; - - wc(`\x1b[${color}m`); - for (let i = 0; i < literals.length; i++) { - w(literals[i]); - if (values[i] !== undefined) { - wc(`\x1b[1m`); - w(String(values[i])); - wc(`\x1b[0;${color}m`); - } - } - wc(`\x1b[0m`); - w('\n'); - }; - -export const logInfo = logColor(2); -export const logWarn = logColor(33); -export const logError = logColor(31); - -// Stolen as #@CK from mtui! -export async function parseOptions(options, optionDescriptorMap) { - // This function is sorely lacking in comments, but the basic usage is - // as such: - // - // options is the array of options you want to process; - // optionDescriptorMap is a mapping of option names to objects that describe - // the expected value for their corresponding options. - // - // Returned is... - // - a mapping of any specified option names to their values - // - a process.exit(1) and error message if there were any issues - // - // Here are examples of optionDescriptorMap to cover all the things you can - // do with it: - // - // optionDescriptorMap: { - // 'telnet-server': {type: 'flag'}, - // 't': {alias: 'telnet-server'} - // } - // - // options: ['t'] -> result: {'telnet-server': true} - // - // optionDescriptorMap: { - // 'directory': { - // type: 'value', - // validate(name) { - // // const whitelistedDirectories = ['apple', 'banana'] - // if (whitelistedDirectories.includes(name)) { - // return true - // } else { - // return 'a whitelisted directory' - // } - // } - // }, - // 'files': {type: 'series'} - // } - // - // ['--directory', 'apple'] -> {'directory': 'apple'} - // ['--directory=banana'] -> {'directory': 'banana'} - // ['--directory', 'artichoke'] -> (error) - // - // ['--files', 'a', 'b', 'c', ';'] -> {'files': ['a', 'b', 'c']} - // ['--files=a,b,c'] -> {'files': ['a', 'b', 'c']} - // ['--files', 'a,b,c'] -> {'files': ['a', 'b', 'c']} - - const handleDashless = optionDescriptorMap[parseOptions.handleDashless]; - const handleUnknown = optionDescriptorMap[parseOptions.handleUnknown]; - - const result = Object.create(null); - for (let i = 0; i < options.length; i++) { - const option = options[i]; - if (option.startsWith('--')) { - // --x can be a flag or expect a value or series of values - let name = option.slice(2).split('=')[0]; // '--x'.split('=') = ['--x'] - let descriptor = optionDescriptorMap[name]; - - if (!descriptor) { - if (handleUnknown) { - handleUnknown(option); - } else { - console.error(`Unknown option name: ${name}`); - process.exit(1); - } - continue; - } - - if (descriptor.alias) { - name = descriptor.alias; - descriptor = optionDescriptorMap[name]; - } - - switch (descriptor.type) { - case 'flag': { - result[name] = true; - break; - } - - case 'value': { - let value = option.slice(2).split('=')[1]; - if (!value) { - value = options[++i]; - if (!value || value.startsWith('-')) { - value = null; - } - } - - if (!value) { - console.error(`Expected a value for --${name}`); - process.exit(1); - } - - result[name] = value; - break; - } - - case 'series': { - if (option.includes('=')) { - result[name] = option.split('=')[1].split(','); - break; - } - - // without a semicolon to conclude the series, - // assume the next option expresses the whole series - if (!options.slice(i).includes(';')) { - let value = options[++i]; - - if (!value || value.startsWith('-')) { - value = null; - } - - if (!value) { - console.error(`Expected values for --${name}`); - process.exit(1); - } - - result[name] = value.split('=')[1].split(','); - break; - } - - const endIndex = i + options.slice(i).indexOf(';'); - result[name] = options.slice(i + 1, endIndex); - i = endIndex; - break; - } - } - - if (descriptor.validate) { - const validation = await descriptor.validate(result[name]); - if (validation !== true) { - console.error(`Expected ${validation} for --${name}`); - process.exit(1); - } - } - } else if (option.startsWith('-')) { - // mtui doesn't use any -x=y or -x y format optionuments - // -x will always just be a flag - let name = option.slice(1); - let descriptor = optionDescriptorMap[name]; - if (!descriptor) { - if (handleUnknown) { - handleUnknown(option); - } else { - console.error(`Unknown option name: ${name}`); - process.exit(1); - } - continue; - } - - if (descriptor.alias) { - name = descriptor.alias; - descriptor = optionDescriptorMap[name]; - } - - if (descriptor.type === 'flag') { - result[name] = true; - } else { - console.error(`Use --${name} (value) to specify ${name}`); - process.exit(1); - } - } else if (handleDashless) { - handleDashless(option); - } - } - return result; -} - -// Takes precisely the same sort of structure as `parseOptions` above, -// and displays associated help messages. Radical! -// -// 'indentWrap' should be the function from '#sugar', with its wrap option -// already bound. -// -// 'sort' should take care of sorting a list of {name, descriptor} entries. -export function showHelpForOptions({ - heading, - options, - indentWrap, - sort = entries => entries, -}) { - if (heading) { - console.log(colors.bright(heading)); - } - - const sortedOptions = - sort( - Object.entries(options) - .map(([name, descriptor]) => ({name, descriptor}))); - - if (!sortedOptions.length) { - console.log(`(No options available)`) - } - - let justInsertedPaddingLine = false; - - for (const {name, descriptor} of sortedOptions) { - if (descriptor.alias) { - continue; - } - - const aliases = - Object.entries(options) - .filter(([_name, {alias}]) => alias === name) - .map(([name]) => name); - - let wrappedHelp, wrappedHelpLines = 0; - if (descriptor.help) { - wrappedHelp = indentWrap(descriptor.help, {spaces: 4}); - wrappedHelpLines = wrappedHelp.split('\n').length; - } - - if (wrappedHelpLines > 0 && !justInsertedPaddingLine) { - console.log(''); - } - - console.log(colors.bright(` --` + name) + - (aliases.length - ? ` (or: ${aliases.map(alias => colors.bright(`--` + alias)).join(', ')})` - : '') + - (descriptor.help - ? '' - : colors.dim(' (no help provided)'))); - - if (wrappedHelp) { - console.log(wrappedHelp); - } - - if (wrappedHelpLines > 1) { - console.log(''); - justInsertedPaddingLine = true; - } else { - justInsertedPaddingLine = false; - } - } - - if (!justInsertedPaddingLine) { - console.log(``); - } -} - -export const handleDashless = Symbol(); -export const handleUnknown = Symbol(); - -export function decorateTime(arg1, arg2) { - const [id, functionToBeWrapped] = - typeof arg1 === 'string' || typeof arg1 === 'symbol' - ? [arg1, arg2] - : [Symbol(arg1.name), arg1]; - - const meta = decorateTime.idMetaMap[id] ?? { - wrappedName: functionToBeWrapped.name, - timeSpent: 0, - timesCalled: 0, - displayTime() { - const align1 = 48; - const align2 = 22; - - const averageTime = (meta.timeSpent / meta.timesCalled).toExponential(1); - const idPart = typeof id === 'symbol' ? id.description : id; - const timePart = `${meta.timeSpent} ms / ${meta.timesCalled} calls`; - const avgPart = `(avg: ${averageTime} ms)`; - - const alignPart1 = - (idPart.length >= align1 - ? ' ' - : ' ' + '.'.repeat(Math.max(0, align1 - 2 - idPart.length)) + ' '); - - const alignPart2 = - (timePart.length >= align2 - ? ' ' - : ' '.repeat(Math.max(0, align2 - timePart.length))); - - console.log( - colors.bright(idPart) + - alignPart1 + - timePart + - alignPart2 + - colors.dim(avgPart)); - }, - }; - - decorateTime.idMetaMap[id] = meta; - - const fn = function (...args) { - const start = Date.now(); - const ret = functionToBeWrapped.apply(this, args); - const end = Date.now(); - meta.timeSpent += end - start; - meta.timesCalled++; - return ret; - }; - - fn.displayTime = meta.displayTime; - - return fn; -} - -decorateTime.idMetaMap = Object.create(null); - -decorateTime.displayTime = function () { - const map = decorateTime.idMetaMap; - - const keys = [ - ...Object.getOwnPropertySymbols(map), - ...Object.getOwnPropertyNames(map), - ]; - - if (!keys.length) { - return; - } - - console.log(`\x1b[1mdecorateTime results: ` + '-'.repeat(40) + '\x1b[0m'); - - const metas = - keys - .map(key => map[key]) - .filter(meta => meta.timeSpent >= 1) // Milliseconds! - .sort((a, b) => a.timeSpent - b.timeSpent); - - for (const meta of metas) { - meta.displayTime(); - } -}; - -export function progressPromiseAll(msgOrMsgFn, array) { - if (!array.length) { - return Promise.resolve([]); - } - - const msgFn = - typeof msgOrMsgFn === 'function' ? msgOrMsgFn : () => msgOrMsgFn; - - let done = 0, - total = array.length; - process.stdout.write(`\r${msgFn()} [0/${total}]`); - const start = Date.now(); - return Promise.all( - array.map((promise) => - Promise.resolve(promise).then((val) => { - done++; - // const pc = `${done}/${total}`; - const pc = (Math.round((done / total) * 1000) / 10 + '%').padEnd( - '99.9%'.length, - ' ' - ); - if (done === total) { - const time = Date.now() - start; - process.stdout.write( - `\r\x1b[2m${msgFn()} [${pc}] \x1b[0;32mDone! \x1b[0;2m(${time} ms) \x1b[0m\n` - ); - } else { - process.stdout.write(`\r${msgFn()} [${pc}] `); - } - return val; - }) - ) - ); -} - -export function progressCallAll(msgOrMsgFn, array) { - if (!array.length) { - return []; - } - - const msgFn = - typeof msgOrMsgFn === 'function' ? msgOrMsgFn : () => msgOrMsgFn; - - const updateInterval = 1000 / 60; - - let done = 0, - total = array.length; - process.stdout.write(`\r${msgFn()} [0/${total}]`); - const start = Date.now(); - const vals = []; - let lastTime = 0; - - for (const fn of array) { - const val = fn(); - done++; - - if (done === total) { - const pc = '100%'.padEnd('99.9%'.length, ' '); - const time = Date.now() - start; - process.stdout.write( - `\r\x1b[2m${msgFn()} [${pc}] \x1b[0;32mDone! \x1b[0;2m(${time} ms) \x1b[0m\n` - ); - } else if (Date.now() - lastTime >= updateInterval) { - const pc = (Math.round((done / total) * 1000) / 10 + '%').padEnd('99.9%'.length, ' '); - process.stdout.write(`\r${msgFn()} [${pc}] `); - lastTime = Date.now(); - } - vals.push(val); - } - - return vals; -} - -export function fileIssue({ - topMessage = `This shouldn't happen.`, -} = {}) { - if (topMessage) { - console.error(colors.red(`${topMessage} Please let the HSMusic developers know:`)); - } - console.error(colors.red(`- https://hsmusic.wiki/feedback/`)); - console.error(colors.red(`- https://github.com/hsmusic/hsmusic-wiki/issues/`)); -} - -export async function logicalCWD() { - if (process.env.PWD) { - return process.env.PWD; - } - - const {exec} = await import('node:child_process'); - const {stat} = await import('node:fs/promises'); - - try { - await stat('/bin/sh'); - } catch (error) { - // Not logical, so sad. - return process.cwd(); - } - - const proc = exec('/bin/pwd -L'); - - let output = ''; - proc.stdout.on('data', buf => { output += buf; }); - - await new Promise(resolve => proc.on('exit', resolve)); - - return output.trim(); -} - -export async function logicalPathTo(target) { - const {relative} = await import('node:path'); - const cwd = await logicalCWD(); - return relative(cwd, target); -} - -export function stringifyCache(cache) { - cache ??= {}; - - if (Object.keys(cache).length === 0) { - return `{}`; - } - - const entries = Object.entries(cache); - sortByName(entries, {getName: entry => entry[0]}); - - return [ - `{`, - entries - .map(([key, value]) => [JSON.stringify(key), JSON.stringify(value)]) - .map(([key, value]) => `${key}: ${value}`) - .map((line, index, array) => - (index < array.length - 1 - ? `${line},` - : line)) - .map(line => ` ${line}`), - `}`, - ].flat().join('\n'); -} |