Repository: pinojs/pino-pretty (js)

Issue:
PinoPretty.customPrettifiers() lacks arguments declared in type PinoPretty.Prettifier
Here is the type defenition:
<img width="1025" alt="Знімок екрана 2024-03-24 о 19 29 34" src="https://github.com/pinojs/pino-pretty/assets/68164497/972b3a35-7a1f-4620-b2e2-1c93398236e6">

And here is JS code for PinoPretty.customPrettifiers(): 
<img width="1208" alt="Знімок екрана 2024-03-24 о 19 31 22" src="https://github.com/pinojs/pino-pretty/assets/68164497/6fc7c104-ad1b-40b2-b9cd-4235c8a1cf20">

As you can see we are lacking of injection of all arguments from type defenition

Pull request:
fix: add injection of lacking arguments of customPrettifiers
resolves #500

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b2c9cd9..9e2d5f9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -40,8 +40,14 @@ jobs:
       contents: read
     strategy:
       matrix:
-        node-version: [14, 16, 18]
+        node-version: [14, 16, 18.17, 20, 22]
         os: [ubuntu-latest]
+        pino-version: [8.20.0, ^8.21.0, ^9.0.0]
+        exclude:
+          - node-version: 14
+            pino-version: ^9.0.0
+          - node-version: 16
+            pino-version: ^9.0.0
     steps:
       - name: Check out repo
         uses: actions/checkout@v4
@@ -62,6 +68,9 @@ jobs:
       - name: Install dependencies
         run: npm i --ignore-scripts
 
+      - name: Install pino ${{ matrix.pino-version }}
+        run: npm i --no-save pino@${{ matrix.pino-version }}
+
       - name: Run tests
         run: npm run ci
 
diff --git a/Readme.md b/Readme.md
index d09e66c..5004029 100644
--- a/Readme.md
+++ b/Readme.md
@@ -230,6 +230,19 @@ module.exports = opts => require('pino-pretty')({
 })
 ```
 
+### Checking color support in TTY
+
+This boolean returns whether the currently used TTY supports colorizing the logs.
+
+```js
+import pretty from 'pino-pretty'
+
+if (pretty.isColorSupported) {
+  ...
+}
+
+```
+
 <a id="options"></a>
 ### Options
 
@@ -240,10 +253,10 @@ The options accepted have keys corresponding to the options described in [CLI Ar
   colorize: colorette.isColorSupported, // --colorize
   colorizeObjects: true, //--colorizeObjects
   crlf: false, // --crlf
-  errorLikeObjectKeys: ['err', 'error'], // --errorLikeObjectKeys
+  errorLikeObjectKeys: ['err', 'error'], // --errorLikeObjectKeys (not required to match custom errorKey with pino >=8.21.0)
   errorProps: '', // --errorProps
   levelFirst: false, // --levelFirst
-  messageKey: 'msg', // --messageKey
+  messageKey: 'msg', // --messageKey (not required with pino >=8.21.0)
   levelKey: 'level', // --levelKey
   messageFormat: false, // --messageFormat
   timestampKey: 'time', // --timestampKey
@@ -253,7 +266,7 @@ The options accepted have keys corresponding to the options described in [CLI Ar
   hideObject: false, // --hideObject
   singleLine: false, // --singleLine
   customColors: 'err:red,info:blue', // --customColors
-  customLevels: 'err:99,info:1', // --customLevels
+  customLevels: 'err:99,info:1', // --customLevels (not required with pino >=8.21.0)
   levelLabel: 'levelLabel', // --levelLabel
   minimumLevel: 'info', // --minimumLevel
   useOnlyCustomProps: true, // --useOnlyCustomProps
diff --git a/index.d.ts b/index.d.ts
index 07dc628..eecdbf2 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -71,6 +71,8 @@ interface PrettyOptions_ {
   /**
    * The key in the JSON object to use as the highlighted message.
    * @default "msg"
+   *
+   * Not required when used with pino >= 8.21.0
    */
   messageKey?: string;
   /**
@@ -122,6 +124,8 @@ interface PrettyOptions_ {
   /**
    * Define the log keys that are associated with error like objects.
    * @default ["err", "error"]
+   *
+   * Not required to handle custom errorKey when used with pino >= 8.21.0
    */
   errorLikeObjectKeys?: string[];
   /**
@@ -192,6 +196,8 @@ interface PrettyOptions_ {
    *
    * @example ( CSV ) customLevels: 'info:10,some_level:40'
    * @example ( Object ) customLevels: { info: 10, some_level: 40 }
+   *
+   * Not required when used with pino >= 8.21.0
    */
   customLevels?: string|object;
   /**
@@ -207,6 +213,7 @@ interface PrettyOptions_ {
 }
 
 declare function build(options: PrettyOptions_): PinoPretty.PrettyStream;
+type isColorSupported = PinoPretty.isColorSupported
 
 declare namespace PinoPretty {
   type Prettifier<T = object> = (inputData: string | object, key: string, log: object, extras: PrettifierExtras<T>) => string;
@@ -218,7 +225,8 @@ declare namespace PinoPretty {
   type ColorizerFactory = typeof colorizerFactory;
   type PrettyFactory = typeof prettyFactory;
   type Build = typeof build;
+  type isColorSupported = typeof Colorette.isColorSupported;
 }
 
 export default PinoPretty;
-export { build, PinoPretty, PrettyOptions_ as PrettyOptions, colorizerFactory, prettyFactory };
+export { build, PinoPretty, PrettyOptions_ as PrettyOptions, colorizerFactory, prettyFactory, isColorSupported };
diff --git a/index.js b/index.js
index 3e500f8..31d8fa8 100644
--- a/index.js
+++ b/index.js
@@ -131,8 +131,18 @@ function prettyFactory (options) {
  * @returns {Transform | (Transform & OnUnknown)}
  */
 function build (opts = {}) {
-  const pretty = prettyFactory(opts)
+  let pretty = prettyFactory(opts)
   return abstractTransport(function (source) {
+    source.on('message', function pinoConfigListener (message) {
+      if (!message || message.code !== 'PINO_CONFIG') return
+      Object.assign(opts, {
+        messageKey: message.config.messageKey,
+        errorLikeObjectKeys: Array.from(new Set([...(opts.errorLikeObjectKeys || ERROR_LIKE_KEYS), message.config.errorKey])),
+        customLevels: message.config.levels.values
+      })
+      pretty = prettyFactory(opts)
+      source.off('message', pinoConfigListener)
+    })
     const stream = new Transform({
       objectMode: true,
       autoDestroy: true,
@@ -168,4 +178,5 @@ module.exports = build
 module.exports.build = build
 module.exports.prettyFactory = prettyFactory
 module.exports.colorizerFactory = colors
+module.exports.isColorSupported = isColorSupported
 module.exports.default = build
diff --git a/lib/pretty.js b/lib/pretty.js
index ebb4380..5284d7b 100644
--- a/lib/pretty.js
+++ b/lib/pretty.js
@@ -138,7 +138,7 @@ function pretty (inputData) {
   }
 
   // pino@7+ does not log this anymore
-  if (log.type === 'Error' && log.stack) {
+  if (log.type === 'Error' && typeof log.stack === 'string') {
     const prettifiedErrorLog = prettifyErrorLog({ log, context: this.context })
     if (this.singleLine) line += this.EOL
     line += prettifiedErrorLog
diff --git a/lib/utils/prettify-metadata.js b/lib/utils/prettify-metadata.js
index 5fb4a3b..72483b1 100644
--- a/lib/utils/prettify-metadata.js
+++ b/lib/utils/prettify-metadata.js
@@ -21,18 +21,22 @@ module.exports = prettifyMetadata
  * returned. Otherwise, a string of prettified metadata is returned.
  */
 function prettifyMetadata ({ log, context }) {
-  const prettifiers = context.customPrettifiers
+  const { customPrettifiers: prettifiers, colorizer } = context
   let line = ''
 
   if (log.name || log.pid || log.hostname) {
     line += '('
 
     if (log.name) {
-      line += prettifiers.name ? prettifiers.name(log.name) : log.name
+      line += prettifiers.name
+        ? prettifiers.name(log.name, 'name', log, { colors: colorizer.colors })
+        : log.name
     }
 
     if (log.pid) {
-      const prettyPid = prettifiers.pid ? prettifiers.pid(log.pid) : log.pid
+      const prettyPid = prettifiers.pid
+        ? prettifiers.pid(log.pid, 'pid', log, { colors: colorizer.colors })
+        : log.pid
       if (log.name && log.pid) {
         line += '/' + prettyPid
       } else {
@@ -43,14 +47,22 @@ function prettifyMetadata ({ log, context }) {
     if (log.hostname) {
       // If `pid` and `name` were in the ignore keys list then we don't need
       // the leading space.
-      line += `${line === '(' ? 'on' : ' on'} ${prettifiers.hostname ? prettifiers.hostname(log.hostname) : log.hostname}`
+      const prettyHostname = prettifiers.hostname
+        ? prettifiers.hostname(log.hostname, 'hostname', log, { colors: colorizer.colors })
+        : log.hostname
+
+      line += `${line === '(' ? 'on' : ' on'} ${prettyHostname}`
     }
 
     line += ')'
   }
 
   if (log.caller) {
-    line += `${line === '' ? '' : ' '}<${prettifiers.caller ? prettifiers.caller(log.caller) : log.caller}>`
+    const prettyCaller = prettifiers.caller
+      ? prettifiers.caller(log.caller, 'caller', log, { colors: colorizer.colors })
+      : log.caller
+
+    line += `${line === '' ? '' : ' '}<${prettyCaller}>`
   }
 
   if (line === '') {
diff --git a/package.json b/package.json
index 7dd7fa0..5ea45e4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "pino-pretty",
-  "version": "11.0.0",
+  "version": "11.1.0",
   "description": "Prettifier for Pino log lines",
   "type": "commonjs",
   "main": "index.js",
@@ -36,7 +36,7 @@
   "dependencies": {
     "colorette": "^2.0.7",
     "dateformat": "^4.6.3",
-    "fast-copy": "^3.0.0",
+    "fast-copy": "^3.0.2",
     "fast-safe-stringify": "^2.1.1",
     "help-me": "^5.0.0",
     "joycon": "^3.1.1",
@@ -46,19 +46,20 @@
     "pump": "^3.0.0",
     "readable-stream": "^4.0.0",
     "secure-json-parse": "^2.4.0",
-    "sonic-boom": "^3.0.0",
+    "sonic-boom": "^4.0.1",
     "strip-json-comments": "^3.1.1"
   },
   "devDependencies": {
     "@types/node": "^20.1.0",
     "fastbench": "^1.0.1",
-    "pino": "^8.0.0",
+    "pino": "^9.0.0",
     "pre-commit": "^1.2.2",
     "rimraf": "^3.0.2",
+    "semver": "^7.6.0",
     "snazzy": "^9.0.0",
     "standard": "^17.0.0",
     "tap": "^16.0.0",
-    "tsd": "^0.30.0",
+    "tsd": "^0.31.0",
     "typescript": "^5.0.2"
   },
   "tsd": {


Test patch:
diff --git a/lib/utils/prettify-metadata.test.js b/lib/utils/prettify-metadata.test.js
index 91281a9..cf2f892 100644
--- a/lib/utils/prettify-metadata.test.js
+++ b/lib/utils/prettify-metadata.test.js
@@ -2,8 +2,12 @@
 
 const tap = require('tap')
 const prettifyMetadata = require('./prettify-metadata')
+const getColorizer = require('../colors')
 const context = {
-  customPrettifiers: {}
+  customPrettifiers: {},
+  colorizer: {
+    colors: {}
+  }
 }
 
 tap.test('returns `undefined` if no metadata present', async t => {
@@ -104,8 +108,42 @@ tap.test('uses prettifiers from passed prettifiers object', async t => {
   const str = prettifyMetadata({
     log: { pid: '1234', hostname: 'bar', caller: 'baz', name: 'joe' },
     context: {
-      customPrettifiers: prettifiers
+      customPrettifiers: prettifiers,
+      colorizer: { colors: {} }
     }
   })
   t.equal(str, '(JOE/1234__ on BAR) <BAZ>')
 })
+
+tap.test('uses colorizer from passed context to colorize metadata', async t => {
+  const prettifiers = {
+    name (input, _key, _log, { colors }) {
+      return colors.blue(input)
+    },
+    pid (input, _key, _log, { colors }) {
+      return colors.red(input)
+    },
+    hostname (input, _key, _log, { colors }) {
+      return colors.green(input)
+    },
+    caller (input, _key, _log, { colors }) {
+      return colors.cyan(input)
+    }
+  }
+  const log = { name: 'foo', pid: '1234', hostname: 'bar', caller: 'baz' }
+  const colorizer = getColorizer(true)
+  const context = {
+    customPrettifiers: prettifiers,
+    colorizer
+  }
+
+  const result = prettifyMetadata({ log, context })
+
+  const colorizedName = colorizer.colors.blue(log.name)
+  const colorizedPid = colorizer.colors.red(log.pid)
+  const colorizedHostname = colorizer.colors.green(log.hostname)
+  const colorizedCaller = colorizer.colors.cyan(log.caller)
+  const expected = `(${colorizedName}/${colorizedPid} on ${colorizedHostname}) <${colorizedCaller}>`
+
+  t.equal(result, expected)
+})
diff --git a/test/basic.test.js b/test/basic.test.js
index 0201e69..ede3f70 100644
--- a/test/basic.test.js
+++ b/test/basic.test.js
@@ -11,6 +11,7 @@ const path = require('path')
 const rimraf = require('rimraf')
 const { join } = require('path')
 const fs = require('fs')
+const semver = require('semver')
 const pinoPretty = require('..')
 const SonicBoom = require('sonic-boom')
 const _prettyFactory = pinoPretty.prettyFactory
@@ -27,6 +28,9 @@ function prettyFactory (opts) {
   return _prettyFactory(opts)
 }
 
+const Empty = function () {}
+Empty.prototype = Object.create(null)
+
 // All dates are computed from 'Fri, 30 Mar 2018 17:35:28 GMT'
 const epoch = 1522431328992
 const formattedEpoch = '17:35:28.992'
@@ -918,6 +922,32 @@ test('basic prettifier tests', (t) => {
     t.equal(arst, 'INFO: hello world\n')
   })
 
+  t.test('log error-like object', (t) => {
+    t.plan(7)
+    const expectedLines = [
+      '    type: "Error"',
+      '    message: "m"',
+      '    stack: [',
+      '      "line1",',
+      '      "line2"',
+      '    ]'
+    ]
+    const pretty = prettyFactory()
+    const log = pino({}, new Writable({
+      write (chunk, enc, cb) {
+        const formatted = pretty(chunk.toString())
+        const lines = formatted.split('\n')
+        t.equal(lines.length, expectedLines.length + 2)
+        lines.shift(); lines.pop()
+        for (let i = 0; i < lines.length; i += 1) {
+          t.equal(lines[i], expectedLines[i])
+        }
+        cb()
+      }
+    }))
+    log.error({ type: 'Error', message: 'm', stack: ['line1', 'line2'] })
+  })
+
   t.test('include should override ignore', (t) => {
     t.plan(1)
     const pretty = prettyFactory({ ignore: 'time,level', include: 'time,level' })
@@ -925,6 +955,22 @@ test('basic prettifier tests', (t) => {
     t.equal(arst, `[${formattedEpoch}] INFO: hello world\n`)
   })
 
+  t.test('inlude a single key with null object', (t) => {
+    t.plan(1)
+    const pretty = prettyFactory({ include: 'level' })
+    const obj = new Empty()
+    obj.nested = 'property'
+    const arst = pretty({
+      msg: 'hello world',
+      pid: `${pid}`,
+      hostname,
+      time: epoch,
+      obj,
+      level: 30
+    })
+    t.equal(arst, 'INFO: hello world\n')
+  })
+
   t.test('prettifies trace caller', (t) => {
     t.plan(1)
     const traceCaller = (instance) => {
@@ -1055,7 +1101,7 @@ test('basic prettifier tests', (t) => {
   t.test('stream usage', async (t) => {
     t.plan(1)
     const tmpDir = path.join(__dirname, '.tmp_' + Date.now())
-    t.teardown(() => rimraf(tmpDir, noop))
+    t.teardown(() => rimraf.sync(tmpDir))
 
     const destination = join(tmpDir, 'output')
 
@@ -1083,24 +1129,28 @@ test('basic prettifier tests', (t) => {
   t.test('sync option', async (t) => {
     t.plan(1)
     const tmpDir = path.join(__dirname, '.tmp_' + Date.now())
-    t.teardown(() => rimraf(tmpDir, noop))
+    t.teardown(() => rimraf.sync(tmpDir))
 
     const destination = join(tmpDir, 'output')
 
-    const pretty = pinoPretty({
-      singleLine: true,
-      colorize: false,
-      mkdir: true,
-      append: false,
-      sync: true,
-      destination
-    })
-    const log = pino(pretty)
-    log.info({ msg: 'message', extra: { foo: 'bar', number: 42 }, upper: 'foobar' })
+    const log = pino(pino.transport({
+      target: '..',
+      options: {
+        singleLine: true,
+        colorize: false,
+        mkdir: true,
+        append: false,
+        sync: true,
+        destination
+      }
+    }))
+    log.info({ msg: 'message', extra: { foo: 'bar', number: 43 }, upper: 'foobar' })
+
+    await watchFileCreated(destination)
 
     const formatted = fs.readFileSync(destination, 'utf8')
 
-    t.equal(formatted, `[${formattedEpoch}] INFO (${pid}): message {"extra":{"foo":"bar","number":42},"upper":"foobar"}\n`)
+    t.equal(formatted, `[${formattedEpoch}] INFO (${pid}): message {"extra":{"foo":"bar","number":43},"upper":"foobar"}\n`)
   })
 
   t.test('support custom colors object', async (t) => {
@@ -1129,9 +1179,68 @@ test('basic prettifier tests', (t) => {
     log.info('foo')
   })
 
+  t.test('check support for colors', (t) => {
+    t.plan(1)
+    const isColorSupported = pinoPretty.isColorSupported
+    t.type(isColorSupported, 'boolean')
+  })
+
   t.end()
 })
 
+if (semver.gte(pino.version, '8.21.0')) {
+  test('using pino config', (t) => {
+    t.beforeEach(() => {
+      Date.originalNow = Date.now
+      Date.now = () => epoch
+    })
+    t.afterEach(() => {
+      Date.now = Date.originalNow
+      delete Date.originalNow
+    })
+
+    t.test('can use different message keys', (t) => {
+      t.plan(1)
+      const destination = new Writable({
+        write (formatted, enc, cb) {
+          t.equal(
+            formatted.toString(),
+            `[${formattedEpoch}] INFO (${pid}): baz\n`
+          )
+          cb()
+        }
+      })
+      const pretty = pinoPretty({
+        destination,
+        colorize: false
+      })
+      const log = pino({ messageKey: 'bar' }, pretty)
+      log.info({ bar: 'baz' })
+    })
+
+    t.test('handles customLogLevels', (t) => {
+      t.plan(1)
+      const destination = new Writable({
+        write (formatted, enc, cb) {
+          t.equal(
+            formatted.toString(),
+            `[${formattedEpoch}] TESTCUSTOM (${pid}): test message\n`
+          )
+          cb()
+        }
+      })
+      const pretty = pinoPretty({
+        destination,
+        colorize: false
+      })
+      const log = pino({ customLevels: { testCustom: 35 } }, pretty)
+      log.testCustom('test message')
+    })
+
+    t.end()
+  })
+}
+
 function watchFileCreated (filename) {
   return new Promise((resolve, reject) => {
     const TIMEOUT = 2000
@@ -1152,5 +1261,3 @@ function watchFileCreated (filename) {
     }, INTERVAL)
   })
 }
-
-function noop () {}
diff --git a/test/error-objects.test.js b/test/error-objects.test.js
index e92e7dd..b9840c6 100644
--- a/test/error-objects.test.js
+++ b/test/error-objects.test.js
@@ -5,8 +5,10 @@ process.env.TZ = 'UTC'
 const Writable = require('stream').Writable
 const test = require('tap').test
 const pino = require('pino')
+const semver = require('semver')
 const serializers = pino.stdSerializers
-const _prettyFactory = require('../').prettyFactory
+const pinoPretty = require('../')
+const _prettyFactory = pinoPretty.prettyFactory
 
 function prettyFactory (opts) {
   if (!opts) {
@@ -452,3 +454,46 @@ test('error like objects tests', (t) => {
 
   t.end()
 })
+
+if (semver.gte(pino.version, '8.21.0')) {
+  test('using pino config', (t) => {
+    t.beforeEach(() => {
+      Date.originalNow = Date.now
+      Date.now = () => epoch
+    })
+    t.afterEach(() => {
+      Date.now = Date.originalNow
+      delete Date.originalNow
+    })
+
+    t.test('prettifies Error in custom errorKey', (t) => {
+      t.plan(8)
+      const destination = new Writable({
+        write (chunk, enc, cb) {
+          const formatted = chunk.toString()
+          const lines = formatted.split('\n')
+          t.equal(lines.length, expected.length + 7)
+          t.equal(lines[0], `[${formattedEpoch}] INFO (${pid}): hello world`)
+          t.match(lines[1], /\s{4}customErrorKey: {/)
+          t.match(lines[2], /\s{6}"type": "Error",/)
+          t.match(lines[3], /\s{6}"message": "hello world",/)
+          t.match(lines[4], /\s{6}"stack":/)
+          t.match(lines[5], /\s{6}Error: hello world/)
+          // Node 12 labels the test `<anonymous>`
+          t.match(lines[6], /\s{10}(at Test.t.test|at Test.<anonymous>)/)
+          cb()
+        }
+      })
+      const pretty = pinoPretty({
+        destination,
+        colorize: false
+      })
+      const log = pino({ errorKey: 'customErrorKey' }, pretty)
+      const err = Error('hello world')
+      const expected = err.stack.split('\n')
+      log.info({ customErrorKey: err })
+    })
+
+    t.end()
+  })
+}