npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

babypino

v1.0.0

Published

Minimalist, zero-dependency, pino-compatible logger that writes newline-delimited JSON to stdout — a lightweight drop-in default (with a console-backed browser build).

Readme

babypino

A minimalist, zero-runtime-dependency logger with a pino-compatible surface. It writes newline-delimited JSON to stdout, and nothing else.

What this is (and isn't)

babypino is not trying to be faster than pino, or more capable than pino. It does less, on purpose.

It exists for one niche: libraries that accept a pino logger from their users. Such a library needs a default for when the user passes nothing. A no-op logger hides everything; depending on pino drags a real logging stack (and its dependencies) into every consumer. babypino is the middle ground - an extremely lightweight, zero-dependency default that does the minimum useful thing (write JSON lines to stdout) while exposing pino's Logger shape, so the same code path works whether or not the user supplies a real logger.

function createWidget({ logger = new Babypino() } = {}) {
    logger.info('widget created'); // works with babypino or a real pino instance
}

In production, your users pass a serious pino instance with levels, redaction, transports, and async flushing. babypino is the friendly default, not the destination.

It is deliberately minimal: level gating is a no-op (every call is written), isLevelEnabled() always returns true, and level is stored but never filters. If you need any of pino's real machinery, use pino — see the FAQ.

Install

npm install babypino

Usage

import { Babypino, babypino } from 'babypino'; // or: const { Babypino, babypino } = require('babypino')

const log = new Babypino(); // or babypino() — a drop-in for pino(options?, stream?)

log.info('hello world');
log.info({ reqId: 'abc' }, 'handled %s in %dms', 'GET /', 12);
log.error(new Error('boom'));

const child = log.child({ service: 'api' });
child.warn('low disk');

Output (one compact JSON object per line):

{"level":30,"time":1717761600000,"pid":1234,"hostname":"host","msg":"hello world"}

Options

Matches pino's pino(options?, stream?) signature — the output stream is the second argument (default process.stdout), and the first argument may be the stream on its own:

new Babypino(); // or babypino()
new Babypino(options);
new Babypino(stream); // bare stream
new Babypino(options, stream);

| option | default | effect | | ------------ | ---------------------------- | --------------------------------------------------------------------- | | colour | stream.getColorDepth() > 1 | ANSI colour on/off, or a partial Record<Level, string> of overrides | | base | { pid, hostname } | static fields added to every line; null to omit | | name | — | added to every line as a name field | | enabled | true | when false, the logger is a no-op | | level | 'info' | initial level label — stored & returned, but does not gate output | | msgPrefix | — | string prefixed to every message; children concatenate onto it | | crlf | false | use \r\n line endings instead of \n | | timestamp | true | include the epoch-millis time field; false omits it | | messageKey | 'msg' | key the message is written under | | errorKey | 'err' | key the serialized error is written under | | nestedKey | — | nest a merged object under this key instead of spreading it |

These are all output-shaping options — they change the line babypino writes, nothing more. Any other pino option (e.g. redact, serializers, transport) type-checks via a catch-all index signature but has no effect.

logger.version reports the pino API version babypino targets (e.g. 10.x), not babypino's own package version. Some pino-aware code reads logger.version to feature-detect, so it mirrors pino rather than this package.

pino-compatible behaviour

  • Object-first merge: log.info({ a: 1 }, 'msg') merges { a: 1 } into the record.
  • Error-first: log.error(err) serialises to err: { type, message, stack, ...ownProps } and sets msg to err.message when no explicit message is given.
  • util.format interpolation for the message and trailing args.
  • Numeric levels (trace:10 … fatal:60) and epoch-millis time, matching pino's defaults.
  • Child loggers inherit parent bindings (child keys win) and share the stream.

The surface (trace/debug/info/warn/error/fatal/silent, child, bindings, setBindings, isLevelEnabled, level, levels, flush) is structurally compatible with pino's Logger. This is enforced at build time — see test/types.test-d.ts, which fails to type-check if the surface ever drifts from import('pino').Logger.

Browser

There is a separate, console-backed build (like pino's own browser.js). Bundlers pick it up automatically via the package's browser export — your import doesn't change:

import { babypino } from 'babypino'; // resolves to the browser build when bundling for the web

It is intentionally different from the Node build: no JSON serialisation, no pid/hostname/time, no streams. Each call is forwarded to the matching console method (with the logger's bindings prepended), so DevTools render objects and errors natively:

babypino().child({ reqId: 'r1' }).info({ items: 3 }, 'cart updated');
// → console.info({ reqId: 'r1' }, { items: 3 }, 'cart updated')

Same Babypino / babypino API and types; it honours level, enabled, msgPrefix, and onChild (the record-shaping options don't apply to console). Zero node: imports, so it bundles with nothing to polyfill.

Because there is no node:events in the browser, the build can't extend EventEmitter — but, like pino's browser logger, it still exposes on/off/ emit/once/addListener/… as no-ops, so consumer code that calls logger.on('level-change', …) won't throw (those events simply never fire).

FAQ

Do you support transports (pino.transport)?

Does it spark joy?

Do you support redaction (redact)?

Does it spark joy?

Do you support custom serializers?

Does it spark joy?

Do you support pretty-printing (pino-pretty)?

Does it spark joy?

Do you support asynchronous logging / sonic-boom?

Which part of "minimalist" keeps tripping you up?

Do you support custom levels (customLevels)?

Which part of "minimalist" keeps tripping you up?

Do you support level filtering / isLevelEnabled?

isLevelEnabled returns true. Always.

Do you support pino.multistream?

Which part of "minimalist" keeps tripping you up?

Do you support mixin, hooks, or formatters?

Which part of "minimalist" keeps tripping you up?

This is a serious production logger though, right? No. That's the whole point. Pass a real pino in production; keep this as the default.

Development

Requires Node.js 18+ (the dev toolchain — tsx/tsup/mocha — needs it; c8 coverage needs 20+). The shipped runtime itself supports Node 16+.

npm run build      # bundle src/ → dist (ESM + CJS + types) via tsup
npm run lint       # strict, type-aware ESLint (typescript-eslint)
npm run typecheck  # tsc --noEmit (implementation + pino compatibility check)
npm test           # prettier + lint + typecheck, then mocha with c8 coverage
npm run bench      # microbenchmark: babypino vs real pino

The package ships zero runtime dependencies; everything above is dev-only.

License

MIT