From 7676b4d799bf0111bfc46b0d1ee9d589625468b9 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Thu, 7 May 2026 14:56:54 +1000 Subject: [PATCH] feat!: convert to ESM, require Node.js >= 20 --- .github/dependabot.yml | 4 + .github/workflows/test-and-release.yml | 38 +-- .gitignore | 1 + README.md | 20 +- bole.js | 12 +- format.js | 6 +- package.json | 27 +- test.js | 390 +++++++++++++------------ 8 files changed, 261 insertions(+), 237 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f468993..167aca9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,6 +7,8 @@ updates: commit-message: prefix: 'chore' include: 'scope' + cooldown: + default-days: 5 - package-ecosystem: 'npm' directory: '/' schedule: @@ -14,3 +16,5 @@ updates: commit-message: prefix: 'chore' include: 'scope' + cooldown: + default-days: 5 diff --git a/.github/workflows/test-and-release.yml b/.github/workflows/test-and-release.yml index c71e28e..e174edf 100644 --- a/.github/workflows/test-and-release.yml +++ b/.github/workflows/test-and-release.yml @@ -1,5 +1,6 @@ name: Test & Maybe Release on: [push, pull_request] + jobs: test: strategy: @@ -10,17 +11,20 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout Repository - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6 - name: Use Node.js ${{ matrix.node }} - uses: actions/setup-node@v6.4.0 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} - name: Install Dependencies + run: npm install --no-progress + - name: Check build is up to date run: | - npm install --no-progress + npm run build + git diff --exit-code || (echo "::error::Build artifacts not committed. Run 'npm run build' and commit the changes." && exit 1) - name: Run tests - run: | - npm test + run: npm test + release: name: Release needs: test @@ -28,37 +32,25 @@ jobs: if: github.event_name == 'push' && github.ref == 'refs/heads/master' permissions: contents: write + issues: write + pull-requests: write id-token: write steps: - name: Checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup Node.js - uses: actions/setup-node@v6.4.0 + uses: actions/setup-node@v6 with: node-version: lts/* registry-url: 'https://registry.npmjs.org' - name: Install dependencies - run: | - npm install --no-progress --no-package-lock --no-save + run: npm install --no-progress --no-package-lock --no-save - name: Build - run: | - npm run build - - name: Install plugins - run: | - npm install \ - @semantic-release/commit-analyzer \ - conventional-changelog-conventionalcommits \ - @semantic-release/release-notes-generator \ - @semantic-release/npm \ - @semantic-release/github \ - @semantic-release/git \ - @semantic-release/changelog \ - --no-progress --no-package-lock --no-save + run: npm run build - name: Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_CONFIG_PROVENANCE: true run: npx semantic-release - diff --git a/.gitignore b/.gitignore index 3c3629e..d5f19d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +package-lock.json diff --git a/README.md b/README.md index 82e2718..71f6cb7 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ **A tiny JSON logger, optimised for speed and simplicity** -[![CI](https://github.com/rvagg/bole/actions/workflows/test-and-release.yml/badge.svg)](https://github.com/rvagg/bole/actions/workflows/test-and-release.yml) - [![NPM](https://nodei.co/npm/bole.svg?style=flat&data=n,v&color=blue)](https://nodei.co/npm/bole/) Log JSON from within Node.js applications. The log format is obviously inspired by the excellent [Bunyan](https://github.com/trentm/node-bunyan) and is likely to be output-compatible in most cases. The difference is that **bole** aims for even more simplicity, supporting only the common-case basics. @@ -14,9 +12,11 @@ Log JSON from within Node.js applications. The log format is obviously inspired **mymodule.js** ```js -const log = require('bole')('mymodule') +import bole from 'bole' + +const log = bole('mymodule') -module.exports.derp = () => { +export function derp () { log.debug('W00t!') log.info('Starting mymodule#derp()') } @@ -24,15 +24,15 @@ module.exports.derp = () => { **main.js** ```js -const bole = require('bole') -const mod = require('./mymodule') +import bole from 'bole' +import { derp } from './mymodule.js' bole.output({ level: 'info', stream: process.stdout }) -mod.derp() +derp() ``` ```text @@ -82,10 +82,14 @@ If you require more sophisticated serialisation of your objects, then write a ut The `logger` object returned by `bole(name)` is also a function that accepts a `name` argument. It returns a new logger whose name is the parent logger with the new name appended after a `':'` character. This is useful for splitting a logger up for grouping events. Consider the HTTP server case where you may want to group all events from a particular request together: ```js +import http from 'node:http' +import { randomUUID } from 'node:crypto' +import bole from 'bole' + const log = bole('server') http.createServer((req, res) => { - req.log = log(uuid.v4()) // make a new sub-logger + req.log = log(randomUUID()) // make a new sub-logger req.log.info(req) //... diff --git a/bole.js b/bole.js index c2fb156..27af9ad 100644 --- a/bole.js +++ b/bole.js @@ -1,10 +1,10 @@ -'use strict' +import _stringify from 'fast-safe-stringify' +import * as os from 'node:os' +import format from './format.js' -const _stringify = require('fast-safe-stringify') -const individual = require('individual')('$$bole', { fastTime: false }) // singleton -const format = require('./format') +// stash on globalThis so duplicate bole copies in a dep tree share one registry +const individual = (globalThis.$$bole ??= { fastTime: false }) const levels = 'debug info warn error'.split(' ') -const os = require('os') const pid = process.pid let hasObjMode = false const scache = [] @@ -233,4 +233,4 @@ bole.setFastTime = function setFastTime (b) { return bole } -module.exports = bole +export default bole diff --git a/format.js b/format.js index 160dbf4..2211330 100644 --- a/format.js +++ b/format.js @@ -1,8 +1,8 @@ // consider this a warning about getting obsessive about optimization -const utilformat = require('util').format +import { format as utilformat } from 'node:util' -function format (a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16) { +export default function format (a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16) { if (a16 !== undefined) { return utilformat(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16) } @@ -50,5 +50,3 @@ function format (a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a1 } return a1 } - -module.exports = format diff --git a/package.json b/package.json index ed59b3d..f589fa6 100644 --- a/package.json +++ b/package.json @@ -2,11 +2,17 @@ "name": "bole", "version": "5.0.29", "description": "A tiny JSON logger", + "type": "module", "main": "bole.js", + "exports": "./bole.js", + "engines": { + "node": ">=20" + }, "scripts": { "lint": "standard", - "test": "npm run lint && node test.js", - "build": "true" + "build": "true", + "test:unit": "node --test test.js", + "test": "npm run lint && npm run test:unit" }, "keywords": [ "logging", @@ -19,15 +25,18 @@ "url": "https://github.com/rvagg/bole.git" }, "dependencies": { - "fast-safe-stringify": "^2.0.7", - "individual": "^3.0.0" + "fast-safe-stringify": "^2.1.1" }, "devDependencies": { - "bl": "^6.0.0", - "hyperquest": "^2.1.3", - "list-stream": "^2.0.0", - "standard": "^17.0.0", - "tape": "^5.5.3" + "@semantic-release/changelog": "^6.0.3", + "@semantic-release/commit-analyzer": "^13.0.1", + "@semantic-release/git": "^10.0.1", + "@semantic-release/github": "^12.0.6", + "@semantic-release/npm": "^13.1.5", + "@semantic-release/release-notes-generator": "^14.1.0", + "conventional-changelog-conventionalcommits": "^9.3.1", + "semantic-release": "^25.0.3", + "standard": "^17.1.2" }, "release": { "branches": [ diff --git a/test.js b/test.js index 643b971..bc67c06 100644 --- a/test.js +++ b/test.js @@ -1,11 +1,43 @@ -const http = require('http') -const hreq = require('hyperquest') -const test = require('tape') -const bl = require('bl') -const listStream = require('list-stream') -const bole = require('./') +import test from 'node:test' +import assert from 'node:assert' +import * as http from 'node:http' +import { Writable } from 'node:stream' +import * as os from 'node:os' +import bole from './bole.js' + const pid = process.pid -const hostname = require('os').hostname() +const hostname = os.hostname() + +function bufferSink () { + const chunks = [] + const stream = new Writable({ + write (chunk, enc, cb) { + chunks.push(chunk) + cb() + } + }) + Object.defineProperty(stream, 'text', { + get () { return Buffer.concat(chunks).toString() } + }) + return stream +} + +function objectSink () { + const items = [] + const stream = new Writable({ + objectMode: true, + write (chunk, enc, cb) { + items.push(chunk) + cb() + } + }) + Object.defineProperty(stream, 'items', { get () { return items } }) + return stream +} + +function endSink (sink) { + return new Promise((resolve) => sink.end(resolve)) +} function mklogobj (name, level, inp, fastTime) { const out = { @@ -42,11 +74,10 @@ function prepareExpected (expected) { }, '') } -test('test simple logging', (t) => { - t.plan(1) - t.on('end', bole.reset) +test('test simple logging', async (t) => { + t.after(() => bole.reset()) - const sink = bl() + const sink = bufferSink() const log = bole('simple') const expected = [] @@ -64,16 +95,14 @@ test('test simple logging', (t) => { expected.push(mklogobj('simple', 'error', { anError: 'object' })) log.error({ anError: 'object' }) - sink.end(() => { - t.equal(safe(sink.slice().toString()), safe(prepareExpected(expected))) - }) + await endSink(sink) + assert.strictEqual(safe(sink.text), safe(prepareExpected(expected))) }) -test('test complex object logging', (t) => { - t.plan(1) - t.on('end', bole.reset) +test('test complex object logging', async (t) => { + t.after(() => bole.reset()) - const sink = bl() + const sink = bufferSink() const log = bole('simple') const expected = [] const cplx = { @@ -89,16 +118,14 @@ test('test complex object logging', (t) => { expected.push(mklogobj('simple', 'debug', cplx)) log.debug(cplx) - sink.end(() => { - t.equal(safe(sink.slice().toString()), safe(prepareExpected(expected))) - }) + await endSink(sink) + assert.strictEqual(safe(sink.text), safe(prepareExpected(expected))) }) -test('test multiple logs', (t) => { - t.plan(1) - t.on('end', bole.reset) +test('test multiple logs', async (t) => { + t.after(() => bole.reset()) - const sink = bl() + const sink = bufferSink() const log1 = bole('simple1') const log2 = bole('simple2') const expected = [] @@ -117,19 +144,17 @@ test('test multiple logs', (t) => { expected.push(mklogobj('simple2', 'error', { anError: 'object' })) log2.error({ anError: 'object' }) - sink.end(() => { - t.equal(safe(sink.slice().toString()), safe(prepareExpected(expected))) - }) + await endSink(sink) + assert.strictEqual(safe(sink.text), safe(prepareExpected(expected))) }) -test('test multiple outputs', (t) => { - t.plan(4) - t.on('end', bole.reset) +test('test multiple outputs', async (t) => { + t.after(() => bole.reset()) - const debugSink = bl() - const infoSink = bl() - const warnSink = bl() - const errorSink = bl() + const debugSink = bufferSink() + const infoSink = bufferSink() + const warnSink = bufferSink() + const errorSink = bufferSink() const log = bole('simple') const expected = [] @@ -165,27 +190,24 @@ test('test multiple outputs', (t) => { expected.push(mklogobj('simple', 'error', { anError: 'object' })) log.error({ anError: 'object' }) - debugSink.end() - infoSink.end() - warnSink.end() - errorSink.end(() => { - // debug - t.equal(safe(debugSink.slice().toString()), safe(prepareExpected(expected))) - // info - t.equal(safe(infoSink.slice().toString()), safe(prepareExpected(expected.slice(1)))) - // warn - t.equal(safe(warnSink.slice().toString()), safe(prepareExpected(expected.slice(2)))) - // error - t.equal(safe(errorSink.slice().toString()), safe(prepareExpected(expected.slice(3)))) - }) + await Promise.all([ + endSink(debugSink), + endSink(infoSink), + endSink(warnSink), + endSink(errorSink) + ]) + + assert.strictEqual(safe(debugSink.text), safe(prepareExpected(expected))) + assert.strictEqual(safe(infoSink.text), safe(prepareExpected(expected.slice(1)))) + assert.strictEqual(safe(warnSink.text), safe(prepareExpected(expected.slice(2)))) + assert.strictEqual(safe(errorSink.text), safe(prepareExpected(expected.slice(3)))) }) -test('test string formatting', (t) => { - t.plan(8) - t.on('end', bole.reset) +test('test string formatting', async (t) => { + t.after(() => bole.reset()) - function testSingle (level, msg, args) { - const sink = bl() + async function testSingle (level, msg, args) { + const sink = bufferSink() const log = bole('strfmt') bole.output({ @@ -194,34 +216,32 @@ test('test string formatting', (t) => { }) const expected = mklogobj('strfmt', level, msg) - log[level].apply(log, args) + log[level](...args) - sink.end(() => { - t.equal(safe(sink.slice().toString()), safe(prepareExpected(expected))) - }) + await endSink(sink) + assert.strictEqual(safe(sink.text), safe(prepareExpected(expected))) bole.reset() } - testSingle('debug', {}, []) - testSingle('debug', { message: 'test' }, ['test']) - testSingle('info', { message: 'true' }, [true]) - testSingle('info', { message: 'false' }, [false]) - testSingle('warn', { message: 'a number [42]' }, ['a number [%d]', 42]) - testSingle('error', { message: 'a string [str]' }, ['a string [%s]', 'str']) - testSingle( - 'error' - , { message: 'a string [str], a number [101], s, 1, 2 a b c' } - , ['a string [%s], a number [%d], %s, %s, %s', 'str', 101, 's', 1, 2, 'a', 'b', 'c'] + await testSingle('debug', {}, []) + await testSingle('debug', { message: 'test' }, ['test']) + await testSingle('info', { message: 'true' }, [true]) + await testSingle('info', { message: 'false' }, [false]) + await testSingle('warn', { message: 'a number [42]' }, ['a number [%d]', 42]) + await testSingle('error', { message: 'a string [str]' }, ['a string [%s]', 'str']) + await testSingle( + 'error', + { message: 'a string [str], a number [101], s, 1, 2 a b c' }, + ['a string [%s], a number [%d], %s, %s, %s', 'str', 101, 's', 1, 2, 'a', 'b', 'c'] ) - testSingle('error', { message: 'foo bar baz' }, ['foo', 'bar', 'baz']) + await testSingle('error', { message: 'foo bar baz' }, ['foo', 'bar', 'baz']) }) -test('test error formatting', (t) => { - t.plan(1) - t.on('end', bole.reset) +test('test error formatting', async (t) => { + t.after(() => bole.reset()) - const sink = bl() + const sink = bufferSink() const log = bole('errfmt') const err = new Error('error msg in here') @@ -239,17 +259,15 @@ test('test error formatting', (t) => { }) log.debug(err) - sink.end(() => { - const act = safe(sink.slice().toString()).replace(/("stack":")Error:[^"]+/, '$1STACK') - t.equal(act, safe(prepareExpected(expected))) - }) + await endSink(sink) + const act = safe(sink.text).replace(/("stack":")Error:[^"]+/, '$1STACK') + assert.strictEqual(act, safe(prepareExpected(expected))) }) -test('test error formatting with message', (t) => { - t.plan(1) - t.on('end', bole.reset) +test('test error formatting with message', async (t) => { + t.after(() => bole.reset()) - const sink = bl() + const sink = bufferSink() const log = bole('errfmt') const err = new Error('error msg in here') @@ -268,103 +286,112 @@ test('test error formatting with message', (t) => { }) log.debug(err, 'this is a %s', 'message') - sink.end(() => { - const act = safe(sink.slice().toString()).replace(/("stack":")Error:[^"]+/, '$1STACK') - t.equal(act, safe(prepareExpected(expected))) - }) + await endSink(sink) + const act = safe(sink.text).replace(/("stack":")Error:[^"]+/, '$1STACK') + assert.strictEqual(act, safe(prepareExpected(expected))) }) -test('test request object', (t) => { - t.on('end', bole.reset) +test('test request object', async (t) => { + t.after(() => bole.reset()) - const sink = bl() + const sink = bufferSink() const log = bole('reqfmt') - let host bole.output({ level: 'info', stream: sink }) - const server = http.createServer((req, res) => { - const expected = mklogobj('reqfmt', 'info', { - req: { - method: 'GET', - url: '/foo?bar=baz', - headers: { - host, - connection: 'close' - }, - remoteAddress: '127.0.0.1', - remotePort: 'RPORT' - } + await new Promise((resolve, reject) => { + const server = http.createServer((req, res) => { + const host = `${server.address().address}:${server.address().port}` + const expected = mklogobj('reqfmt', 'info', { + req: { + method: 'GET', + url: '/foo?bar=baz', + headers: { + host, + connection: 'close' + }, + remoteAddress: '127.0.0.1', + remotePort: 'RPORT' + } + }) + log.info(req) + + res.end() + + sink.end(() => { + const act = safe(sink.text).replace(/("remotePort":)\d+/, '$1"RPORT"') + try { + assert.strictEqual(act, safe(prepareExpected(expected))) + server.close(resolve) + } catch (err) { + server.close(() => reject(err)) + } + }) }) - log.info(req) - - res.end() - sink.end(() => { - const act = safe(sink.slice().toString()).replace(/("remotePort":)\d+/, '$1"RPORT"') - t.equal(act, safe(prepareExpected(expected))) - server.close(t.end.bind(t)) + server.listen(0, '127.0.0.1', () => { + const host = `${server.address().address}:${server.address().port}` + http.get(`http://${host}/foo?bar=baz`, { agent: false }).on('error', reject) }) }) - - server.listen(0, '127.0.0.1', () => { - host = `${server.address().address}:${server.address().port}` - hreq.get(`http://${host}/foo?bar=baz`) - }) }) -test('test request object with message', (t) => { - t.on('end', bole.reset) +test('test request object with message', async (t) => { + t.after(() => bole.reset()) - const sink = bl() + const sink = bufferSink() const log = bole('reqfmt') - let host bole.output({ level: 'info', stream: sink }) - const server = http.createServer((req, res) => { - const expected = mklogobj('reqfmt', 'info', { - message: 'this is a message', - req: { - method: 'GET', - url: '/foo?bar=baz', - headers: { - host, - connection: 'close' - }, - remoteAddress: '127.0.0.1', - remotePort: 'RPORT' - } + await new Promise((resolve, reject) => { + const server = http.createServer((req, res) => { + const host = `${server.address().address}:${server.address().port}` + const expected = mklogobj('reqfmt', 'info', { + message: 'this is a message', + req: { + method: 'GET', + url: '/foo?bar=baz', + headers: { + host, + connection: 'close' + }, + remoteAddress: '127.0.0.1', + remotePort: 'RPORT' + } + }) + log.info(req, 'this is a %s', 'message') + + res.end() + + sink.end(() => { + const act = safe(sink.text).replace(/("remotePort":)\d+/, '$1"RPORT"') + try { + assert.strictEqual(act, safe(prepareExpected(expected))) + server.close(resolve) + } catch (err) { + server.close(() => reject(err)) + } + }) }) - log.info(req, 'this is a %s', 'message') - res.end() - - sink.end(() => { - const act = safe(sink.slice().toString()).replace(/("remotePort":)\d+/, '$1"RPORT"') - t.equal(act, safe(prepareExpected(expected))) - - server.close(t.end.bind(t)) + server.listen(0, '127.0.0.1', () => { + const host = `${server.address().address}:${server.address().port}` + http.get(`http://${host}/foo?bar=baz`, { agent: false }).on('error', reject) }) }) - - server.listen(0, '127.0.0.1', () => { - host = `${server.address().address}:${server.address().port}` - hreq.get(`http://${host}/foo?bar=baz`) - }) }) -test('test sub logger', (t) => { - t.plan(1) - t.on('end', bole.reset) +test('test sub logger', async (t) => { + t.after(() => bole.reset()) - const sink = bl() + const sink = bufferSink() const log = bole('parent') const expected = [] let sub1 @@ -393,15 +420,14 @@ test('test sub logger', (t) => { expected.push(mklogobj('parent:sub2:subsub', 'error', { anError: 'object' })) sub2('subsub').error({ anError: 'object' }) - sink.end(() => { - t.equal(safe(sink.slice().toString()), safe(prepareExpected(expected))) - }) + await endSink(sink) + assert.strictEqual(safe(sink.text), safe(prepareExpected(expected))) }) -test('test object logging', (t) => { - t.on('end', bole.reset) +test('test object logging', async (t) => { + t.after(() => bole.reset()) - const sink = listStream.obj() + const sink = objectSink() const log = bole('simple') const expected = [] @@ -419,22 +445,20 @@ test('test object logging', (t) => { expected.push(mklogobj('simple', 'error', { anError: 'object' })) log.error({ anError: 'object' }) - sink.end(() => { - t.equal(sink.length, expected.length, 'correct number of log entries') - for (let i = 0; i < expected.length; i++) { - const actual = sink.get(i) - actual.time = safeTime(actual.time) - expected[i].time = safeTime(actual.time) - t.deepEqual(actual, expected[i], `correct log entry #${i}`) - } - t.end() - }) + await endSink(sink) + assert.strictEqual(sink.items.length, expected.length, 'correct number of log entries') + for (let i = 0; i < expected.length; i++) { + const actual = sink.items[i] + actual.time = safeTime(actual.time) + expected[i].time = safeTime(actual.time) + assert.deepStrictEqual(actual, expected[i], `correct log entry #${i}`) + } }) -test('test error and object logging', (t) => { - t.on('end', bole.reset) +test('test error and object logging', async (t) => { + t.after(() => bole.reset()) - const sink = bl() + const sink = bufferSink() const log = bole('errobjfmt') const err = new Error('anError') @@ -454,21 +478,16 @@ test('test error and object logging', (t) => { } }))) - sink.end(() => { - let act = safe(sink.slice().toString()) - - act = act.replace(/("stack":")Error:[^"]+/, '$1STACK') - - t.equal(act, expected) - t.end() - }) + await endSink(sink) + let act = safe(sink.text) + act = act.replace(/("stack":")Error:[^"]+/, '$1STACK') + assert.strictEqual(act, expected) }) -test('test fast time', (t) => { - t.plan(1) - t.on('end', bole.reset) +test('test fast time', async (t) => { + t.after(() => bole.reset()) - const sink = bl() + const sink = bufferSink() const log = bole('simple') const expected = [] @@ -488,16 +507,14 @@ test('test fast time', (t) => { expected.push(mklogobj('simple', 'error', { anError: 'object' }, true)) log.error({ anError: 'object' }) - sink.end(() => { - t.equal(safe(sink.slice().toString()), safe(prepareExpected(expected))) - }) + await endSink(sink) + assert.strictEqual(safe(sink.text), safe(prepareExpected(expected))) }) -test('test undefined values', (t) => { - t.plan(1) - t.on('end', bole.reset) +test('test undefined values', async (t) => { + t.after(() => bole.reset()) - const sink = bl() + const sink = bufferSink() const log = bole('simple') const expected = [] @@ -509,7 +526,6 @@ test('test undefined values', (t) => { expected.push(mklogobj('simple', 'debug', { message: 'testing', aDebug: undefined })) log.debug({ aDebug: undefined }, 'testing') - sink.end(() => { - t.equal(safe(sink.slice().toString()), safe(prepareExpected(expected))) - }) + await endSink(sink) + assert.strictEqual(safe(sink.text), safe(prepareExpected(expected))) })