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

whynotjs

v0.1.0

Published

Track why, when, and where your JavaScript objects changed — like Git blame for plain objects.

Readme

WhyNotJS

You're staring at a bug. Some property is wrong. You have no idea who changed it, when, or from where.

console.log everywhere. Breakpoints. git blame on a file that touches the object in 6 places. Thirty minutes later you find it.

There's a better way.

import { track, why } from "whynotjs";

const user = track({ name: "Hritik", age: 25 });

// …mutations happen across your entire codebase…

why(user, "name");
// Changed 2 times
//
// 1. Old → Hritik  New → John
//    Source: ProfileForm.tsx:24  (handleSubmit)
//    Time:   10:22 PM
//
// 2. Old → John    New → Jane
//    Source: UserSettings.tsx:87  (onSave)
//    Time:   10:45 PM

One line to opt in. No store. No actions. No boilerplate. Works on any plain JavaScript object.


Install

npm install whynotjs

How it works

WhyNotJS wraps your object in a native Proxy. Every mutation fires the set trap, which captures an Error stack trace at that exact moment, parses out the call-site, and stores a ChangeRecord in a WeakMap keyed to your object.

track(obj)
  └─ new Proxy(obj, { set, deleteProperty })
       └─ mutation fires → new Error().stack
            └─ parse call-site  (file · line · col · fn)
                 └─ push ChangeRecord into WeakMap<target, Map<prop, history>>
                      └─ why(obj, prop)  reads it back

Four deliberate design decisions:

  • WeakMap as the store — your object can be garbage-collected normally. No leaks, no cleanup required (unless you want it with untrack).
  • Error.stack for call-sites — no bundler plugin, no source-map server, no build step. It's just a standard V8/SpiderMonkey stack trace, parsed at runtime.
  • Reflect alongside Proxy — every trap delegates through Reflect so prototype chains, getters, and class instances all behave correctly.
  • No-op on identical valuesobj.x = obj.x records nothing. The guard is oldValue !== newValue before any write.

This is the same primitive Vue 3's reactive(), MobX, and Immer are built on. WhyNotJS just exposes the audit trail instead of hiding it.


API

track(obj, options?)

Wrap an object. Returns a Proxy — use it everywhere you'd use the original.

const user   = track({ name: "Hritik", age: 25 });
const config = track({ theme: "dark" }, { verbose: true });

| Option | Type | Default | Description | |---|---|---|---| | maxHistory | number | 50 | Sliding-window cap per property. Oldest records are evicted. | | onchange | (record: ChangeRecord) => void | noop | Fires on every mutation. | | verbose | boolean | false | Logs every change to the console automatically. | | ignore | Array<string \| RegExp> | [] | Properties to skip. | | watch | Array<string \| symbol> | [] | When set, only these properties are tracked. |


why(obj, property)

Full change history for one property.

const report = why(user, "name");

report.count                       // 2
report.changes[0].oldValue         // "Hritik"
report.changes[0].newValue         // "John"
report.changes[0].source           // { file: "ProfileForm.tsx", line: 24, col: 14, fn: "handleSubmit" }
report.changes[0].time             // "10:22 PM"
report.changes[0].timestamp        // "2024-01-15T22:22:00.000Z"

whyAll(obj)

History for every property at once.

const all = whyAll(user);
// { name: WhyReport, age: WhyReport }

print(obj, property)

Pretty-prints to the console using console.group.

print(user, "name");
// [WhyNotJS] .name changed 2 times
//   1. 10:22 PM
//      Old → Hritik  /  New → John
//      ProfileForm.tsx:24 (handleSubmit)
//   2. 10:45 PM
//      Old → John  /  New → Jane
//      UserSettings.tsx:87 (onSave)

reset(obj, property?)

Clear history for one property, or all of them.

reset(user, "name");   // clears .name only
reset(user);           // clears everything

untrack(obj)

Stop tracking entirely and free the WeakMap entry.

untrack(user);

Recipes

React — debug a form without touching state

import { useRef } from "react";
import { track, print } from "whynotjs";

function ProfileForm() {
  const user = useRef(track({ name: "", email: "" })).current;

  return (
    <input
      onChange={e => {
        user.name = e.target.value;       // tracked
        print(user, "name");              // log it any time
      }}
    />
  );
}

Reactive callback — build a mini event system

const store = track(
  { status: "idle", retries: 0 },
  {
    onchange({ property, oldValue, newValue, source, time }) {
      logger.info(`[${time}] ${String(property)}: ${oldValue} → ${newValue}`, source);
    },
  }
);

Audit trail — know exactly what your async code touched

const order = track({ status: "pending", total: 0 });

await processPayment(order);   // black box

print(order, "status");
// Changed 3 times:  pending → validating → charging → complete
// Each with source file + line

Bounded history — keep only the last N changes

const sensor = track({ temp: 0 }, { maxHistory: 10 });

// Stream in 1000 readings — only the last 10 are kept
readings.forEach(r => (sensor.temp = r));

why(sensor, "temp").changes.length   // 10

Ignore internals with a regex

const model = track(obj, { ignore: [/^_/, /^__/] });
// _id, __proto__, _rev — all ignored
// Only public properties are tracked

TypeScript

Written in TypeScript. Full types included, no @types/ package needed.

import type { ChangeRecord, WhyReport, WhyNotOptions, SourceInfo } from "whynotjs";

Compatibility

Requires native Proxy (ES2015+). Cannot be polyfilled.

| Runtime | Minimum version | |---|---| | Node.js | 14 | | Chrome / Edge | 49 | | Firefox | 18 | | Safari | 10 |


License

MIT