« get me outta code hell

Merge branch 'commentary-entries' into preview - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/util/sugar.js
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2023-11-24 15:05:03 -0400
committer(quasar) nebula <qznebula@protonmail.com>2023-11-24 15:05:03 -0400
commiteab0e06d148b5445feab453b8042d5e93e1fa1a2 (patch)
tree62dab7600a89ca73cd1bffc19092c97fe4d58450 /src/util/sugar.js
parentca30b07b9e116f6d42d6ea6a2623e1500c289383 (diff)
parent9f58ba688c8a6ac3acf7b4bc435e2ccaed20b011 (diff)
Merge branch 'commentary-entries' into preview
Diffstat (limited to 'src/util/sugar.js')
-rw-r--r--src/util/sugar.js146
1 files changed, 118 insertions, 28 deletions
diff --git a/src/util/sugar.js b/src/util/sugar.js
index 3f0eb2e..eab44b7 100644
--- a/src/util/sugar.js
+++ b/src/util/sugar.js
@@ -250,6 +250,16 @@ export function typeAppearance(value) {
   return typeof value;
 }
 
+// Limits a string to the desired length, filling in an ellipsis at the end
+// if it cuts any text off.
+export function cut(text, length = 40) {
+  if (text.length >= length) {
+    return text.slice(0, Math.max(1, length - 3)) + '...';
+  } else {
+    return text;
+  }
+}
+
 // Binds default values for arguments in a {key: value} type function argument
 // (typically the second argument, but may be overridden by providing a
 // [bindOpts.bindIndex] argument). Typically useful for preparing a function for
@@ -315,6 +325,12 @@ export function openAggregate({
   // 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.
+  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
@@ -397,7 +413,13 @@ export function openAggregate({
 
   aggregate.close = () => {
     if (errors.length) {
-      throw Reflect.construct(errorClass, [errors, message]);
+      const error = Reflect.construct(errorClass, [errors, message]);
+
+      if (translucent) {
+        error[Symbol.for(`hsmusic.aggregate.translucent`)] = true;
+      }
+
+      throw error;
     }
   };
 
@@ -570,34 +592,101 @@ export function _withAggregate(mode, aggregateOpts, fn) {
 export function showAggregate(topError, {
   pathToFileURL = f => f,
   showTraces = true,
+  showTranslucent = showTraces,
   print = true,
 } = {}) {
-  const recursive = (error, {level}) => {
-    let headerPart = showTraces
-      ? `[${error.constructor.name || 'unnamed'}] ${
-          error.message || '(no message)'
-        }`
-      : error instanceof AggregateError
-      ? `[${error.message || '(no message)'}]`
-      : error.message || '(no message)';
+  const translucentSymbol = Symbol.for('hsmusic.aggregate.translucent');
+
+  const determineCause = error => {
+    let cause = error.cause;
+    if (showTranslucent) return cause ?? null;
+
+    while (cause) {
+      if (!cause[translucentSymbol]) return cause;
+      cause = cause.cause;
+    }
+
+    return null;
+  };
+
+  const determineErrors = parentError => {
+    if (!parentError.errors) return null;
+    if (showTranslucent) return parentError.errors;
+
+    const errors = [];
+    for (const error of parentError.errors) {
+      if (!error[translucentSymbol]) {
+        errors.push(error);
+        continue;
+      }
+
+      if (error.cause) {
+        errors.push(determineCause(error));
+      }
+
+      if (error.errors) {
+        errors.push(...determineErrors(error));
+      }
+    }
+
+    return errors;
+  };
+
+  const flattenErrorStructure = (error, level = 0) => {
+    const cause = determineCause(error);
+    const errors = determineErrors(error);
+
+    return {
+      level,
+
+      kind: error.constructor.name,
+      message: error.message,
+      stack: error.stack,
+
+      cause:
+        (cause
+          ? flattenErrorStructure(cause, level + 1)
+          : null),
+
+      errors:
+        (errors
+          ? errors.map(error => flattenErrorStructure(error, level + 1))
+          : null),
+    };
+  };
+
+  const recursive = ({level, kind, message, stack, cause, errors}) => {
+    const messagePart =
+      message || `(no message)`;
+
+    const kindPart =
+      kind || `unnamed kind`;
+
+    let headerPart =
+      (showTraces
+        ? `[${kindPart}] ${messagePart}`
+     : errors
+        ? `[${messagePart}]`
+        : messagePart);
 
     if (showTraces) {
-      const stackLines = error.stack?.split('\n');
+      const stackLines =
+        stack?.split('\n');
 
-      const stackLine = stackLines?.find(
-        (line) =>
+      const stackLine =
+        stackLines?.find(line =>
           line.trim().startsWith('at') &&
           !line.includes('sugar') &&
           !line.includes('node:') &&
-          !line.includes('<anonymous>')
-      );
+          !line.includes('<anonymous>'));
 
-      const tracePart = stackLine
-        ? '- ' +
-          stackLine
-            .trim()
-            .replace(/file:\/\/.*\.js/, (match) => pathToFileURL(match))
-        : '(no stack trace)';
+      const tracePart =
+        (stackLine
+          ? '- ' +
+            stackLine
+              .trim()
+              .replace(/file:\/\/.*\.js/, (match) => pathToFileURL(match))
+          : '(no stack trace)');
 
       headerPart += ` ${colors.dim(tracePart)}`;
     }
@@ -606,8 +695,8 @@ export function showAggregate(topError, {
     const bar1 = ' ';
 
     const causePart =
-      (error.cause
-        ? recursive(error.cause, {level: level + 1})
+      (cause
+        ? recursive(cause)
             .split('\n')
             .map((line, i) => i === 0 ? ` ${head1} ${line}` : ` ${bar1} ${line}`)
             .join('\n')
@@ -616,19 +705,20 @@ export function showAggregate(topError, {
     const head2 = level % 2 === 0 ? '\u257f' : colors.dim('\u257f');
     const bar2 = level % 2 === 0 ? '\u2502' : colors.dim('\u254e');
 
-    const aggregatePart =
-      (error instanceof AggregateError
-        ? error.errors
-            .map(error => recursive(error, {level: level + 1}))
+    const errorsPart =
+      (errors
+        ? errors
+            .map(error => recursive(error))
             .flatMap(str => str.split('\n'))
             .map((line, i) => i === 0 ? ` ${head2} ${line}` : ` ${bar2} ${line}`)
             .join('\n')
         : '');
 
-    return [headerPart, causePart, aggregatePart].filter(Boolean).join('\n');
+    return [headerPart, causePart, errorsPart].filter(Boolean).join('\n');
   };
 
-  const message = recursive(topError, {level: 0});
+  const structure = flattenErrorStructure(topError);
+  const message = recursive(structure);
 
   if (print) {
     console.error(message);