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

build-meta

v0.1.1

Published

Writes a meta.json with the package version, build date, environment, git branch, and last commit for the current project

Readme

build-meta

Stamps a build with its own provenance: package version, build date, environment, git branch, and the last commit's author and hash.

By default it writes a meta.js that installs the metadata on window['build-meta'] when the page loads, plus a meta.d.ts that types it, so nothing has to be imported or wired up by hand. A meta.json is available too, on request.

  • No runtime dependencies
  • No bundler plugin and no framework coupling
  • Typed out of the box
  • Runs on Bun or Node 24+, needs git on the PATH

Contents

Install

bun add -d build-meta   # or: npm i -D build-meta

Note The version currently on npm (0.0.12) is the older implementation, which depends on yargs, moment, moment-timezone, jsonfile, and several git helpers. The dependency-free rewrite documented here lives in this repository and has not been published yet.

Quick start

Run it from the directory that holds your package.json, and point --src-folder at the folder that should receive the files:

build-meta --src-folder src

That writes src/meta.js and src/meta.d.ts. Wire it into your build:

{
  "scripts": {
    "prebuild": "build-meta --src-folder src --env production"
  }
}

Then load it once, from your entry point:

import './meta.js';

or from your HTML:

<script src="/meta.js"></script>

That is the whole integration. There is no import binding to thread through your app and no assignment to write:

// no longer necessary
import meta from './meta.json';

window.yourcompany = window.yourcompany || {};
window.yourcompany.meta = window.yourcompany.meta || meta;

Using the metadata

Once the file has loaded, the metadata is on the global under the build-meta key:

const meta = window['build-meta'];

console.log(meta.version); // "1.2.3"
console.log(meta.lastCommitHash); // "907761dd591f6bc3a69a514092acfb8c147b73cf"

The key contains a hyphen, so it is reachable as window['build-meta'] and never as window.build-meta, which is not valid JavaScript.

Three details are worth knowing:

  • It does not overwrite. The assignment is g['build-meta'] = g['build-meta'] || { … }, so if something already claimed the key, that value survives.
  • It does not need a window. Where there is no window at all, such as during server-side rendering, it falls back to globalThis instead of throwing.
  • It leaks nothing else. The file is a plain script wrapped in an IIFE, so it works from a <script> tag and from a side-effect import alike, and adds no other global.

Here is the generated file in full:

(function () {
  var g = typeof window !== 'undefined' ? window : globalThis;
  g["build-meta"] = g["build-meta"] || {
    "version": "1.2.3",
    "buildDate": "09-06-2026 12:42:23 PM ET",
    "buildDateISO": "2026-09-06T16:42:23.349Z",
    "buildEnv": "production",
    "branchName": "main",
    "lastCommitAuthor": "CJ Rivas",
    "lastCommitHash": "907761dd591f6bc3a69a514092acfb8c147b73cf"
  };
})();

TypeScript

A meta.d.ts is written next to meta.js on every run, so there is nothing to install and nothing to declare:

// Generated by build-meta. Describes the object meta.js installs on the global.
type BuildMeta = {
  version: string;
  buildDate: string;
  buildDateISO: string;
  buildEnv: string;
  branchName: string;
  lastCommitAuthor: string;
  lastCommitHash: string;
};

interface Window {
  "build-meta"?: BuildMeta;
}

It is a global script rather than a module, so as long as the folder is covered by your tsconfig.json, window['build-meta'] is typed everywhere with no import:

const version = window['build-meta']?.version; // string | undefined

function report(meta: BuildMeta): string {
  return `${meta.version} on ${meta.branchName}`;
}

The key is optional because nothing guarantees the script has loaded by the time your code reads it. BuildMeta describes the object that was actually emitted, so a package.json with no version produces types with no version either, rather than promising a field that is not there. It is a type alias; Window is the one interface, because augmenting the DOM's Window is declaration merging and only interfaces merge.

The meta object

| Field | Example | Source | | --- | --- | --- | | version | "1.2.3" | The version in the package.json of the working directory. Omitted from every output if that file has none. | | buildDate | "09-06-2026 12:42:23 PM ET" | Wall clock reading in America/Toronto, for a human to read. | | buildDateISO | "2026-09-06T16:42:23.349Z" | The same instant in UTC ISO-8601, for Date.parse and for sorting. | | buildEnv | "production" | --env, else NODE_ENV, else PROFILE, else development. | | branchName | "main" | git rev-parse --abbrev-ref HEAD. | | lastCommitAuthor | "CJ Rivas" | git log -1 --format=%an. | | lastCommitHash | "907761d…" | git rev-parse HEAD, the full 40 characters. |

A few consequences of those sources:

  • version is read from the package.json in the current working directory, not from the git root and not from build-meta's own package. In a monorepo, run it from the package you want described.
  • buildDate is always Eastern Time. The timezone and the format are hardcoded so that stamps from different machines and CI regions stay comparable; buildDateISO is the one to compute with.
  • branchName is the literal string HEAD when the repository is in a detached HEAD state. Many CI systems check out a detached commit, so expect "branchName": "HEAD" there unless a branch is checked out explicitly.

The same object is printed to stdout on every run.

Generating a meta.json

meta.json is opt in. Ask for it with --output-json:

build-meta --src-folder src --output-json

It lands in the current working directory, next to the package.json that was read. meta.js and meta.d.ts are still written as well. To put it somewhere else, name the directory:

build-meta --src-folder src --json-out-dir public

--json-out-dir implies --output-json, so the two flags never have to be passed together. The directory must already exist.

{
  "version": "1.2.3",
  "buildDate": "09-06-2026 12:42:23 PM ET",
  "buildDateISO": "2026-09-06T16:42:23.349Z",
  "buildEnv": "production",
  "branchName": "main",
  "lastCommitAuthor": "CJ Rivas",
  "lastCommitHash": "907761dd591f6bc3a69a514092acfb8c147b73cf"
}

All three outputs are derived from one in-memory object, so they can never disagree.

CLI reference

build-meta --src-folder <dir> [--env <name>] [--output-json] [--json-out-dir <dir>]

| Flag | Required | Default | Description | | --- | --- | --- | --- | | --src-folder <dir> | yes | — | Directory to receive meta.js and meta.d.ts. | | --env <name> | no | NODE_ENV, PROFILE, development | Sets buildEnv. An empty value falls through to the rest of the chain. | | --output-json | no | off | Also write a meta.json. | | --json-out-dir <dir> | no | working directory | Where meta.json goes. Implies --output-json. |

Every path is resolved against the current working directory, never the git root. Relative paths, absolute paths, and --flag=value all work. Target directories must already exist; the CLI does not create them.

Parsing is strict: an unknown flag or a stray positional argument fails the run rather than being ignored, so build-meta src is rejected and there is no positional shorthand.

Failures

Every failure exits with status 1.

| Cause | Output | | --- | --- | | --src-folder missing | build-meta: --src-folder <dir> is required | | Unknown flag, or a positional argument | parseArgs error and a stack trace | | No package.json in the working directory | Module resolution error and a stack trace | | Not a git repository, or git not on the PATH | git's own stderr, then a stack trace | | A target folder that does not exist | ENOENT and a stack trace |

Only the first case is handled with a friendly message; the rest surface as uncaught exceptions.

The git commands all run before anything is written, so a bad --src-folder fails at the last step with the git work already done. meta.js and meta.d.ts are written before meta.json, so a bad --json-out-dir fails with both already on disk. The non-zero exit is what stops the build.

Development

The repository is TypeScript, developed and tested with Bun. All the logic is in src/build-meta.ts; bun run build compiles it to the stdlib-only CommonJS bin/build-meta.js that ships, which is why consumers need neither Bun nor TypeScript.

bun run build        # tsgo -p tsconfig.build.json, writes bin/build-meta.js
bun run typecheck    # tsgo -p tsconfig.json, checks src and test
bun run test         # builds, then runs the suite against the compiled CLI
bun run lint         # oxlint
bun run format       # oxfmt
bun run clean        # rm -rf bin

bin/ is generated and is not committed. bun install rebuilds it through the prepare script, and so does bun publish.

Continuous integration

Two workflows live in .github/workflows/. pull-request-checks.yml runs lint, formatting, typecheck, build and tests on every pull request, then proves the two properties the local loop cannot see: that the compiled bin/build-meta.js runs on a bare Node 24 as well as on Bun, and that the package still has zero runtime dependencies with an emit that reaches for nothing but node: builtins. sanity-check.yml runs the same suite on every push to main and adds a check that the packed tarball installs into an empty project and runs.

Notes on the suite

The suite spawns the compiled CLI in a child process against throwaway git repositories under the OS temp directory, because the CLI does all its work at module load. The generated meta.js is loaded for real in a child process and the global read back, and the generated meta.d.ts is compiled with tsgo against sample consumer code, rather than either being pattern matched as text.