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

unsung

v1.1.1

Published

Display credits and signature in the browser console with custom styling options.

Readme

Unsung.ts

npm license

browser + node framework agnostic zero deps

Unsung is a framework-agnostic way to make project attribution visible from the console. Credit data lives in a small JSON file, styling is provided in code, and the output adapts to the runtime: a full styled console.groupCollapsed in the browser, a lightweight unstyled console.group in Node.js, with zero runtime dependencies.

Built for curious developers who open DevTools on a published project and want to discover the people behind the work.

Created and maintained by Thaddeus Ex Ars.

📑 Table of contents

Features · Installation · Quick start · Data format · Browser and node · Theming · API · Contributing · License

✨ Features

  • 🪧 Data driven: declare credits once in JSON, group people by role automatically
  • 🎨 Themeable: text colors, header background and font sizes, via presets or raw hex
  • 🌍 Runtime aware: styled output in the browser, compact in Node, off by default server side
  • 🔒 Safe: prints once per runtime, never throws, fully typed

📦 Installation

pnpm add unsung

⚡ Quick start

import { printCredits } from 'unsung'

// Usually imported from a generated JSON file:
// import credits from './credits.json'
const credits = {
	header: {
		siteName: 'Foo Studio',
		typeOfProject: 'A web experience',
		clientName: 'Foo'
	},
	sections: [
		{
			title: 'Creative',
			entries: [
				{ role: 'Creative Director', entity: 'Sam Smith', handle: '@samsmith' },
				{ role: 'Designer', entity: 'Marie Chen', handle: '@mchen' },
				{ role: 'UX Engineer', entity: 'Jane Doe', handle: '@janedoe' },
				{ role: 'UX Engineer', entity: 'John Doe' }
			]
		},
		{
			title: 'Technology',
			entries: [{ role: 'Technical Director', entity: 'Thaddeus Ex Ars', handle: '@thddexars' }]
		}
	],
	libs: ['Vite', 'GSAP'],
	footer: { year: '2026', owner: 'Thaddeus Ex Ars' }
}

printCredits({ data: credits })

In the browser this opens a single collapsed group titled Credits with a styled sidebar and formatted text:

📋 Data format

The credits payload is plain data only. No styling lives here, so the same data can be themed differently per project.

type CreditsData = {
	header: {
		siteName: string
		typeOfProject?: string // optional free string: "website", "web experience", "package"...
		clientName?: string // optional
	}
	sections?: {
		// optional
		title: string
		entries: {
			role: string
			entity: string // a person or an organization
			handle?: string // optional pseudo, e.g. "@ThddExArs"
		}[]
	}[]
	libs?: string[] // optional
	footer?: {
		// optional
		year: string
		owner: string // copyright holder shown in the footer
	}
}

A few rules:

  • Only header.siteName is required. Everything else is optional: omit typeOfProject, clientName, sections, libs, or footer to skip them without error.
  • No URLs. They are too noisy in a console. The only extra you can attach to a person is a handle.
  • Roles are grouped within their section: entries that share the same role are printed together, in first seen order.
  • Empty values are skipped. Empty libs are dropped, and the Built with block is omitted when there are none.
  • Every string is sanitized before printing: control characters are stripped, whitespace is trimmed, length is capped, and percent signs are handled so they cannot be read as console format specifiers.

🌍 Browser and node

The runtime is detected automatically.

| Runtime | Output | | ------- | --------------------------------------------------------------------------------------- | | Browser | Full styled console.groupCollapsed: header, every section, libs, footer | | Node | Light unstyled console.group: header, libs, footer only (person sections are skipped) | | Unknown | Nothing is printed |

Node printing is off by default. This matters for server side rendering (Next, Nuxt, SvelteKit), where component code runs on the server on every request. Leaving node off keeps the credits a browser only feature and your server logs clean. Turn it on explicitly when you want it:

printCredits({ data: credits, enablePrintInNode: true })

🎨 Theming

Styling is passed in code through the optional theme. It is merged over a built in default theme, so you only specify what you want to override.

printCredits({
	data: credits,
	theme: {
		colors: {
			// one value for all text, or an object targeting the three levels
			text: {
				level1Title: 'softWhite', // the site name, light on the dark background
				level2Title: 'inkBlue', // section and "Built with" titles, dark
				body: 'charcoal' // names, libs, footer, dark
			},
			background: 'blueNight' // applied to the site name line only
		},
		fontSizes: {
			level1Title: 18, // px
			level2Title: 13, // px
			body: 12 // px
		}
	}
})

Color values

Each color is either a preset name or a raw hex string (#RGB or #RRGGBB).

Text presets, light and dark:

| Light text | Dark text | | --------------- | -------------- | | softWhite | charcoal | | iceBlue | inkBlue | | lightMint | deepForest | | lightLavender | darkBurgundy | | paleYellow | espresso | | softPink | graphite |

Background presets, dark and light:

| Dark background | Light background | | --------------- | ---------------- | | blueNight | paperWhite | | deepGreen | skyMist | | darkPlum | lightSand | | burntAmber | blushLight |

Pair a light background with dark text, or a dark background with light text, for readable contrast.

// a single value applies to all three text levels
theme: { colors: { text: 'charcoal' } }

// raw hex works anywhere a preset name does
theme: { colors: { text: { body: '#DDD6FE' }, background: '#0F172A' } }

The maps and the default theme are exported if you want to read or extend them:

import { DEFAULT_THEME, textColorMap, backgroundColorMap } from 'unsung'

⚙️ API

printCredits(options)

Prints the credits to the console. Returns nothing and never throws.

| Option | Type | Default | Description | | ------------------- | -------------- | --------------- | ----------------------------------------------------------------------------- | | data | CreditsData | required | The credits payload | | theme | ConsoleTheme | DEFAULT_THEME | Styling overrides, merged over the default theme | | groupTitle | string | 'Credits' | Label of the console group wrapping the output | | enablePrintInNode | boolean | false | Enable printing in node environments. Off by default to avoid SSR log spam | | logErrors | boolean | false | Surface internal failures as a console.warn instead of staying silent | | collapsed | boolean | true | In browser, use groupCollapsed (default) or group (open). Node unaffected |

Group title

Use groupTitle to replace the default Credits label with something more specific to your project:

printCredits({ data: credits, groupTitle: 'acme-ui credits' })

Failure behavior

The library is built so it can never break the host app. Every internal error (a malformed hex color, a missing field, bad data) is caught at the single public boundary and swallowed by default, the credits are simply skipped.

Set logErrors: true to surface a console.warn explaining what was skipped, so you can fix your config while debugging. You decide when to enable it, there is no NODE_ENV guessing. The library never throws either way.

Print once

The credits print at most once per runtime. A module re-import, a hot reload, or a component remount will not duplicate them.

👥 Contributing

Please read CONTRIBUTING.md.

📜 License

Apache-2.0 © Ludovic Bouvinet (Thaddeus Ex Ars)