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

@meadown/logger

v1.10.0

Published

A development-focused logger for Node.js and TypeScript — zero dependencies, clickable source links, and API response logging built in.

Readme

meadown/logger — a development-focused logger for Node.js

@meadown/logger

A development-focused logger for Node.js and TypeScript. Built to make your development loop faster and your terminal actually readable.

No dependencies. No config. Import it and you're done.

Why this exists

I kept writing the same console.log wrapper in every project. Every time. Copy, paste, rename. And I still shipped it to production by accident. Unconsciously I still spent ten minutes staring at logs trying to figure out which file they came from.

At some point I just built the thing I always wanted.

One import. No config. No dependencies. It shows you exactly where every log came from, and it gets out of the way when you ship.

It's not trying to be Winston or Pino. No transports, no log levels, no config files. Just a better console.log for the hours you spend in development. One that tells you where things came from and disappears when you ship.

The full story — the problem, the research, every design decision, and everything that got cut — is in docs/STORY.md.

Features

  • Zero dependencies
  • Development-focused — built for the dev experience, not production ops
  • Clickable source link — every log jumps straight to the file and line it came from
  • Tap logging — log any value, promise, or function inline without breaking the expression
  • Color-coded levels[INFO] cyan, [WARN] yellow, [ERROR] red
  • Tree layout — clean, scannable structure in your terminal
  • Collapsible output — cap long dumps with logger.maxLines

Install

pnpm add @meadown/logger
# or
npm install @meadown/logger
# or
yarn add @meadown/logger

Quick start

import logger from "@meadown/logger"

logger("server started", { port: 3000 })
[INFO]
├── server started { port: 3000 }
└── 05-30 04:00:00 PM - (server.ts:5)

Works out of the box. Set NODE_ENV=production when you ship and the logs disappear — no wrappers, no cleanup, nothing to remember.

Single shared import

Create one module in your project and re-export from there. One place to set options, one place to change if you ever need to.

// lib/logger.ts
import logger from "@meadown/logger"

logger.maxLines = 10 // configure once, applies everywhere

export default logger
// anywhere else in your project
import logger from "@/lib/logger"

Use a direct re-export — not a wrapper function. A wrapper breaks the (file:line) link on every log.

// GOOD — location stays honest
export { default as logger } from "@meadown/logger"

// BAD — every log points at this file, not the real caller
export const logger = (...args) => log(...args)

API

┌─────────────────┬─────────────┬──────────────────────────┬───────────────────────────┐
│     Method      │     Tag     │          Params          │          Purpose          │
├─────────────────┼─────────────┼──────────────────────────┼───────────────────────────┤
│ logger()        │ [INFO]      │ ...args: unknown[]       │ general info              │
├─────────────────┼─────────────┼──────────────────────────┼───────────────────────────┤
│ logger.warn()   │ [WARN]      │ ...args: unknown[]       │ something needs attention │
├─────────────────┼─────────────┼──────────────────────────┼───────────────────────────┤
│ logger.error()  │ [ERROR]     │ ...args: unknown[]       │ something broke           │
├─────────────────┼─────────────┼──────────────────────────┼───────────────────────────┤
│ logger.tap()    │ [TAP]       │ value, label?: string    │ log value/promise/fn;     │
│                 │             │                          │ returns as-is             │
├─────────────────┼─────────────┼──────────────────────────┼───────────────────────────┤
│ logger.group()  │ [name]      │ { name: string,          │ consolidate related       │
│                 │             │   type?: LogChannel,     │ items under a label       │
│                 │             │   logs: unknown[] }      │                           │
├─────────────────┼─────────────┼──────────────────────────┼───────────────────────────┤
│ logger.maxLines │ —           │ number                   │ cap output at N lines     │
└─────────────────┴─────────────┴──────────────────────────┴───────────────────────────┘

Every tag is self-describing. You scan the logs and immediately know what each entry is without reading the message. That's the design principle holding the whole API together.

logger()

Your everyday log. Pass it anything — strings, objects, errors, whatever. Works exactly like console.log, just prettier.

logger("server started")
logger("user logged in", { userId: 42, role: "admin" })
[INFO]
├── user logged in { userId: 42, role: 'admin' }
└── 05-30 04:00:00 PM - (server.ts:12)

logger.tap()

The one you reach for when you want to see what's inside something without stopping to assign it to a variable first. Logs it and gives it straight back. Works on any value, any promise, and any function — tap detects what you pass and handles it accordingly.

Sync values

Numbers, strings, objects, anything. Logged immediately and returned as-is. Drop it directly into an expression — no extra variable needed.

server.listen(logger.tap(port, "port"))
logger.tap(config, "loaded config")
[TAP]
├── port 5000
└── 05-30 04:00:00 PM - (server.ts:3)

[TAP]
├── loaded config { host: 'localhost', port: 5432, db: 'app' }
└── 05-30 04:00:00 PM - (server.ts:4)

Promises

The same promise comes back immediately — your await and your code are completely unchanged. Once it settles, timing and the resolved value are logged in the background.

const user = await logger.tap(getUser(1), "getUser")
[TAP]
├── getUser 12ms { id: 1, name: 'Alice', role: 'admin' }
└── 05-30 04:00:00 PM - (server.ts:10)

Void operations like client.set() or del() resolve to undefined — tap logs elapsed time only and omits the value:

await logger.tap(cache.set(key, value), "SET")
[TAP]
├── SET 2ms
└── 05-30 04:00:00 PM - (cache.ts:8)

API response logging

Drop it into any fetch and you get timing, status, size, and the response body — without touching your code at all.

const res = await logger.tap(fetch("https://api.example.com/users/1"), "GET /users/1")
[TAP]
├── GET /users/1
│
│  response:
│  ├── time:   65ms
│  ├── status: 200 OK
│  └── size:   848 B
│
│  body:
│  ├── id:    1
│  ├── name:  Leanne Graham
│  └── email: [email protected]
│
└── 05-30 07:54:26 PM - (api.ts:12)

Was it successful? How long did it take? What came back? All there, without opening DevTools.

API response logging: tap a fetch and see timing, status, size, and body

Functions

Pass a function and get back a wrapper with the same signature. Every time it is called, its arguments are logged first, then the original runs and its return value passes through untouched. Useful for seeing exactly what a callback or event handler actually receives.

client.on("error", logger.tap((err) => {
  this.isConnected = false
}, "Redis error:"))
[TAP]
├── Redis error: Error: connect ECONNREFUSED 127.0.0.1:6379
└── 05-30 04:00:00 PM - (client.ts:8)

Works great with array methods too. Wrap the callback and you can see exactly what each item looks like as it goes through. One log per call, nothing breaks:

const adults = users.filter(logger.tap((u) => u.age >= 18, "filter"))
[TAP]
├── filter { id: 1, name: 'Alice', age: 25 }
└── 05-30 04:00:00 PM - (users.ts:14)

[TAP]
├── filter { id: 2, name: 'Bob', age: 16 }
└── 05-30 04:00:00 PM - (users.ts:14)

One easy mistake: tapping the return value of .on() instead of the handler. .on() returns the emitter, not the callback, so you'd be logging the emitter object once and never see an event fire. Tap detects this, logs a [WARN] pointing at the fix, and returns the emitter unchanged so your code keeps running:

// ✗ taps the emitter — logs a [WARN], the emitter comes back unchanged
logger.tap(emitter.on("error", handler), "error")

// ✓ taps the callback — wrap the function, not the .on() call
emitter.on("error", logger.tap(handler, "error"))

logger.group()

Got a handful of related things to log at once? Group them. One block, one timestamp, one place to look.

logger.group({
  name: "Server setup",
  logs: [
    `Running on port ${port}`,
    `Environment: ${env}`,
    `API: http://localhost:${port}/api`,
  ],
})
[SERVER SETUP]
├── Running on port 5000
├── Environment: development
├── API: http://localhost:5000/api
└── 05-30 04:00:00 PM - (server.ts:23)

Use type to set the channel and tag color. Defaults to "log" (cyan, stdout).

logger.group({
  name: "Validation failed",
  type: "error", // red, stderr
  logs: ["email invalid", "password too short"],
})

logger.group({
  name: "Config warnings",
  type: "warn", // yellow, stderr
  logs: ["deprecated key found", "missing optional field"],
})

logs takes anything — strings, objects, arrays, errors. Each renders exactly as console.log would.

logger.error()

Red tag, goes to stderr. Pass an Error and you get the stack too.

logger.error("database connection failed", new Error("ECONNREFUSED"))
[ERROR]
├── database connection failed Error: ECONNREFUSED
│       at Object.<anonymous> (server.ts:14:18)
└── 05-30 04:00:00 PM - (server.ts:14)

logger.warn()

Yellow tag, stderr. For the things that aren't broken yet.

logger.warn("disk usage above 80%")
logger.warn("deprecated config key", { key: "timeout", use: "timeoutMs" })
[WARN]
├── disk usage above 80%
└── 05-30 04:00:00 PM - (monitor.ts:8)

logger.maxLines

Got a massive object dumping 200 lines? Set this and it cuts off after N lines with a count of what's hidden. Set back to 0 to show everything again.

logger.maxLines = 5 // show 5 lines, then "... N more lines"
logger.maxLines = 0 // default — show everything

Framework compatibility

The (file:line) location in every log line always shows your source file — not a build artifact URL. When your code runs inside a bundler, stack frames can contain synthetic paths that have nothing to do with where you wrote the code. The logger strips these automatically:

| Environment | Stack frame format | What you see | | --- | --- | --- | | Next.js (webpack) | webpack-internal:///(rsc)/./src/app/page.tsx | page.tsx:42 | | Angular CLI / Vue CLI | webpack-internal:///./src/... | page.tsx:42 | | Next.js (Turbopack) | [project]/src/app/page.tsx | page.tsx:42 | | Vite SSR / plain Node.js | real file:// path | page.tsx:42 |

No config needed — the logger detects the format from the stack frame and handles it. Bundler-internal frames (e.g. Turbopack's own runtime files) are filtered out and shown as unknown rather than leaking a meaningless path.

Production

Set NODE_ENV=production and everything goes silent. No wrapper calls, no grep before release, no accidental logs in prod.

| NODE_ENV | Logs? | | ---------------------------------------- | ---------- | | not set, development, or anything else | shown | | production | suppressed |

Security

Zero dependencies, no file or network access, nothing persisted. See SECURITY.md for the full security model.

License

Architected and developed by Dewan Mobashirul

MIT © meadown