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

@soujvnunes/util

v0.4.0

Published

Zero-dependency TypeScript utilities.

Readme

@soujvnunes/util

Zero-dependency TypeScript utilities. Each is a subpath export, so you import exactly what you use. (No npm dependencies: createApi and readNdjson use the platform fetch and ReadableStream.)

Install

pnpm add @soujvnunes/util

| Subpath | Export | What | | --- | --- | --- | | ./ellipses | ellipses | Truncate a string to head...tail | | ./objectHas | objectHas | Narrow an untrusted key to keyof O (own-key check) | | ./devLog | devLog | Scope-tagged console.log, no-op outside NODE_ENV=development | | ./matchesQuery | matchesQuery | Validate and type-narrow searchParams against an allow-list schema | | ./formatTimestamp | formatTimestamp | Intl.DateTimeFormat wrapper (locale-aware) | | ./getErrorMessage | getErrorMessage | Message from an unknown thrown value, else a fallback | | ./isConnectionError | isConnectionError | true for a network failure (ECONNREFUSED / fetch failed) | | ./buildQueryString | buildQueryString | Flat params to ?a=1&b=2 (skips null/undefined) | | ./createApi | createApi (+ envelope builders) | Throw-free fetch factory returning ApiResponse<T> (nextjs-conventions §26) | | ./readNdjson | readNdjson | Async generator over a newline-delimited-JSON ReadableStream |

./ellipses: truncate a string from both ends

ellipses(string, count = 3) keeps count characters from each end and puts ... between them, so the shape of an address or a hash stays recognisable. It throws when the input is shorter than count * 2 + 3, because truncating there would not save any characters.

import { ellipses } from '@soujvnunes/util/ellipses'

ellipses('0x71C7656EC7ab88b098defB751B7401B5f6d8976F') // '0x7...76F'
ellipses('0x71C7656EC7ab88b098defB751B7401B5f6d8976F', 6) // '0x71C7...d8976F'

./objectHas: narrow an untrusted key

objectHas(object, key) is a type predicate over Object.hasOwn. It narrows a runtime string (a route param, a form field name, a search param) to keyof O, so the lookup that follows needs no assertion. The own-key check also rejects a key that is not on the shape, including __proto__.

import { objectHas } from '@soujvnunes/util/objectHas'

const LOCALES = { en: 'English', pt: 'Português' }

export default async function Page({ params }: { params: Promise<{ lang: string }> }) {
  const { lang } = await params

  if (!objectHas(LOCALES, lang)) notFound()

  return <h1>{LOCALES[lang]}</h1> // lang is 'en' | 'pt'
}

./devLog: dev-only trace log

devLog(scope, ...args) prints [scope] plus the args when NODE_ENV is development, and does nothing otherwise. Use it to watch an async pipeline in the terminal without shipping the noise to production. The scope tag keeps interleaved parallel work readable.

import { devLog } from '@soujvnunes/util/devLog'

devLog('extraction', 'page', index, 'of', total)
// dev terminal: [extraction] page 3 of 12

./matchesQuery: validate and narrow searchParams

matchesQuery(query, schema) checks a whole searchParams object against a schema mapping each param to its allowed values, and narrows every param to its literal union. A param is valid when absent or equal to one of the literals. A repeated param arrives as string[] and is rejected.

A schema value that names another schema key marks a dependency: the named param is valid only when its parent holds that value, and is required when it does.

import { matchesQuery } from '@soujvnunes/util/matchesQuery'

const SCHEMA = { action: ['review', 'export'], export: ['csv', 'pdf'] } as const

type Query = Record<string, string | string[] | undefined>

export default async function Page({ searchParams }: { searchParams: Promise<Query> }) {
  const query = await searchParams

  if (!matchesQuery(query, SCHEMA)) notFound()

  // query.action is 'review' | 'export' | undefined, query.export is 'csv' | 'pdf' | undefined
  return (
    <Report
      action={query.action}
      format={query.export}
    />
  )
}

// ?action=export&export=csv  valid
// ?action=export             rejected, the dependent param is required
// ?export=csv                rejected, the parent does not hold 'export'

./formatTimestamp: locale-aware date formatting

formatTimestamp(value, lang) formats a Date or a date string through Intl.DateTimeFormat with a fixed field set: numeric year, short month, 2-digit day, 2-digit hour and minute, 24-hour clock. Pass the request locale so the server and the client agree on the output. The clock time renders in the runtime timezone, so a server component and a browser can differ unless you pin TZ.

import { formatTimestamp } from '@soujvnunes/util/formatTimestamp'

formatTimestamp(entry.createdAt, 'en-US') // 'Aug 05, 2026, 14:30'
formatTimestamp('2026-08-05T14:30:00Z', 'pt-BR') // '05 de ago. de 2026, 14:30'

There is no options argument. A different field set means a local Intl.DateTimeFormat, not a wrapper around this one.

./getErrorMessage: message from an unknown throw

getErrorMessage(error, fallback = 'Something went wrong') returns the message of an Error and the fallback for anything else. Use it at a boundary that must not rethrow, such as a server action returning through useActionState.

import { getErrorMessage } from '@soujvnunes/util/getErrorMessage'

export async function createEntry(_: State, formData: FormData) {
  try {
    return { data: await save(formData) }
  } catch (error) {
    return { error: getErrorMessage(error, 'Could not save the entry.') }
  }
}

./isConnectionError: detect a network failure

isConnectionError(error) is true when the message carries ECONNREFUSED, fetch failed, or Failed to fetch, which is the request never reaching the server. It reads the message because the platform rarely types these. Keep the auth and domain classifiers in the consumer, where their strings live.

It takes unknown and reads the message through getErrorMessage, so a catch binding goes straight in with no instanceof Error narrowing at the call site. A throw that is not an Error has no message to match and returns false.

import { isConnectionError } from '@soujvnunes/util/isConnectionError'

try {
  await connectDb()
} catch (error) {
  if (isConnectionError(error)) return { error: 'Cannot reach the database right now.' }

  throw error
}

Nothing reaching this through createApi throws, because that envelope already carries the message. Use it around a client that does throw, such as Mongoose or a bare fetch.

./buildQueryString: flat params to a query string

buildQueryString(params) serialises a flat object into a leading ? plus the pairs. It skips undefined and null, coerces the rest with String(), and returns '' when the object is empty or nullish, so it concatenates onto a path safely.

import { buildQueryString } from '@soujvnunes/util/buildQueryString'

const url = `/entries${buildQueryString({ page: 2, sort: 'name', cursor: null })}`
// '/entries?page=2&sort=name'

buildQueryString({}) // ''
buildQueryString(undefined) // ''

./createApi: throw-free fetch factory

createApi({ baseURL, headers? }) returns a typed api<T>(endpoint, options?) that always resolves to an ApiResponse<T>. A non-ok status or a network/parse error becomes an error envelope, never a rejection. It ships createApiResponseSuccess and createApiResponseError so the server builds the same shape. Callers branch on .success; a server action returns the message through useActionState, so there is no try/catch and no onError callback (§26/§38).

// shared/lib/api.ts, 'use server'
import { createApi } from '@soujvnunes/util/createApi'

export const api = createApi({ baseURL: process.env.API_URL! })
// const res = await api<User>('/user/1'); if (!res.success) return { error: res.message }

The route handler on the other end builds the same envelope, so api() passes its body straight through. createApiResponseError() defaults to status 404 and message 'Not found'.

// app/api/user/[id]/route.ts
import { createApiResponseError, createApiResponseSuccess } from '@soujvnunes/util/createApi'

export async function GET(_: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  const user = await getUser(id)

  if (!user) return Response.json(createApiResponseError({ message: `No user ${id}.` }))

  return Response.json(createApiResponseSuccess(user))
}

Send that envelope with a 200. On a non-ok HTTP status api() stops reading the body and rebuilds the envelope from response.status and response.statusText, so the message the route wrote is lost. The status the caller reads comes from the envelope either way.

./readNdjson: stream newline-delimited JSON

readNdjson<T>(body) is an async generator over a ReadableStream, yielding one parsed value per \n-terminated line. A partial trailing line is buffered until its newline arrives, and a final unterminated line is yielded on close. Use it client-side to consume a progress stream one event at a time.

import { readNdjson } from '@soujvnunes/util/readNdjson'

const res = await fetch('/api/extract', { method: 'POST', body })

if (!res.body) return

for await (const event of readNdjson<ProgressEvent>(res.body)) {
  setProgress(event.percent)
}