Commit 2c4b5e8

Anton Golub <antongolub@antongolub.com>
2025-03-10 14:59:42
feat(log): provide custom formatting for every entry kind (#1123)
* feat(log): provide custom formatting for every entry kind continues #1122 * chore: ignore unknown log entries
1 parent 85c260c
docs/configuration.md
@@ -108,10 +108,13 @@ Set `log.output` to change the stream.
 $.log.output = process.stdout
 ```
 
-Set `log.formatCmd` to customize the command highlighter:
+Set `log.formatters` to customize each log entry kind printing:
 
 ```ts
-$.log.formatCmd = (cmd: string) => chalk.bgRedBright.black(cmd)
+$.log.formatters = {
+  cmd: (entry: LogEntry) => `CMD: ${entry.cmd}`,
+  fetch: (entry: LogEntry) => `FETCH: ${entry.url}`
+}
 ```
 
 ## `$.timeout`
src/log.ts
@@ -24,7 +24,12 @@ export type LogEntry = {
       id: string
     }
   | {
-      kind: 'stdout' | 'stderr'
+      kind: 'stdout'
+      data: Buffer
+      id: string
+    }
+  | {
+      kind: 'stderr'
       data: Buffer
       id: string
     }
@@ -59,39 +64,60 @@ export type LogEntry = {
     }
 )
 
-type LogFormatter = (cmd?: string) => string
+type LogFormatters = {
+  [key in LogEntry['kind']]: (
+    entry: Extract<LogEntry, { kind: key }>
+  ) => string | Buffer
+}
+
+const formatters: LogFormatters = {
+  cmd({ cmd }) {
+    return formatCmd(cmd)
+  },
+  stdout({ data }) {
+    return data
+  },
+  stderr({ data }) {
+    return data
+  },
+  custom({ data }) {
+    return data
+  },
+  fetch(entry) {
+    const init = entry.init ? ' ' + inspect(entry.init) : ''
+    return '$ ' + chalk.greenBright('fetch') + ` ${entry.url}${init}\n`
+  },
+  cd(entry) {
+    return '$ ' + chalk.greenBright('cd') + ` ${entry.dir}\n`
+  },
+  retry(entry) {
+    return (
+      chalk.bgRed.white(' FAIL ') +
+      ` Attempt: ${entry.attempt}${entry.total == Infinity ? '' : `/${entry.total}`}` +
+      (entry.delay > 0 ? `; next in ${entry.delay}ms` : '') +
+      '\n'
+    )
+  },
+  end() {
+    return ''
+  },
+}
+
 type Log = {
   (entry: LogEntry): void
-  formatCmd?: LogFormatter
+  formatters?: Partial<LogFormatters>
   output?: NodeJS.WriteStream
 }
+
 export const log: Log = function (entry) {
   if (!entry.verbose) return
   const stream = log.output || process.stderr
-  switch (entry.kind) {
-    case 'cmd':
-      stream.write((log.formatCmd || formatCmd)(entry.cmd))
-      break
-    case 'stdout':
-    case 'stderr':
-    case 'custom':
-      stream.write(entry.data)
-      break
-    case 'cd':
-      stream.write('$ ' + chalk.greenBright('cd') + ` ${entry.dir}\n`)
-      break
-    case 'fetch':
-      const init = entry.init ? ' ' + inspect(entry.init) : ''
-      stream.write('$ ' + chalk.greenBright('fetch') + ` ${entry.url}${init}\n`)
-      break
-    case 'retry':
-      stream.write(
-        chalk.bgRed.white(' FAIL ') +
-          ` Attempt: ${entry.attempt}${entry.total == Infinity ? '' : `/${entry.total}`}` +
-          (entry.delay > 0 ? `; next in ${entry.delay}ms` : '') +
-          '\n'
-      )
-  }
+  const format = (log.formatters?.[entry.kind] || formatters[entry.kind]) as (
+    entry: LogEntry
+  ) => string | Buffer
+  if (!format) return // ignore unknown log entries
+  const data = format(entry)
+  stream.write(data)
 }
 
 const SYNTAX = '()[]{}<>;:+|&='
@@ -115,7 +141,7 @@ const RESERVED_WORDS = new Set([
   'EOF',
 ])
 
-export const formatCmd: LogFormatter = function (cmd) {
+export function formatCmd(cmd: string): string {
   if (cmd == undefined) return chalk.grey('undefined')
   let q = ''
   let out = '$ '
test/log.test.ts
@@ -27,7 +27,10 @@ describe('log', () => {
 
     before(() => (log.output = stream))
 
-    after(() => delete log.output)
+    after(() => {
+      delete log.output
+      delete log.formatters
+    })
 
     beforeEach(() => (data.length = 0))
 
@@ -88,6 +91,20 @@ describe('log', () => {
         '\x1B[41m\x1B[37m FAIL \x1B[39m\x1B[49m Attempt: 1/3; next in 1000ms\n'
       )
     })
+
+    test('formatters', () => {
+      log.formatters = {
+        cmd: ({ cmd }) => `CMD: ${cmd}`,
+      }
+
+      log({
+        kind: 'cmd',
+        cmd: 'echo hi',
+        id: '1',
+        verbose: true,
+      })
+      assert.equal(data.join(''), 'CMD: echo hi')
+    })
   })
 
   test('formatCwd()', () => {
.size-limit.json
@@ -16,7 +16,7 @@
   {
     "name": "dts libdefs",
     "path": "build/*.d.ts",
-    "limit": "39.15 kB",
+    "limit": "39.3 kB",
     "brotli": false,
     "gzip": false
   },
@@ -30,7 +30,7 @@
   {
     "name": "all",
     "path": "build/*",
-    "limit": "850.8 kB",
+    "limit": "851.1 kB",
     "brotli": false,
     "gzip": false
   }