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

@zakkster/lite-di-graph

v1.0.0

Published

Read-only formatters/exporters over a lite-di-container describe() snapshot: JSON, Graphviz DOT, and Chrome Trace Event Format. Pure over the snapshot, fail-closed on a malformed shape.

Readme

@zakkster/lite-di-graph

Read-only formatters/exporters over a @zakkster/lite-di-container describe() snapshot: JSON, Graphviz DOT, and Chrome Trace Event Format. PURE over the snapshot, fail-closed on a malformed shape.

npm version sponsor Fail-Closed npm bundle size npm downloads npm total downloads Tree-Shakeable TypeScript Dependencies license

The graph exporter the DI ecosystem was missing

@zakkster/lite-di-container builds, wires, and tears down your object graph, and since v2.1.0 it can hand you a describe() snapshot of that graph: { nodes, edges, order }. What it did not have was a way to SEE it -- to render the snapshot as a Graphviz diagram, a round-trippable JSON document, or a Perfetto trace. The container should not carry three serializers; that is a separate, read-only concern.

lite-di-graph is that concern. It is a pure formatter: hand it a snapshot (or a booted container via fromContainer) and it returns a string. It never touches container private state, and every exporter fails CLOSED on a malformed snapshot rather than emitting half-valid output.

npm install @zakkster/lite-di-graph

Peer dependency (not bundled, install it alongside):

npm install @zakkster/lite-di-container
import { Container } from '@zakkster/lite-di-container';
import { fromContainer, toDOT } from '@zakkster/lite-di-graph';

const c = new Container();
c.value('cfg', { tag: 'T' });
c.singleton('svc', class { constructor(cfg) { this.cfg = cfg; } }, ['cfg']);
c.boot();

const dot = toDOT(fromContainer(c));   // a Graphviz `digraph` string
// pipe `dot` to `dot -Tsvg` to render the dependency graph

Table of contents

Why this exists

The container validates and wires the graph; a snapshot only exists AFTER a successful boot(), so by the time you can format one it is already known-valid. That leaves a legibility gap: you have a correct graph in memory and no way to look at it. Serializing it well is finicky -- factory deps are opaque closures a consumer must not mistake for leaves, aliases resolve to a target, tokens can be symbols, and the output has to be deterministic to diff. That belongs in a small, single-purpose, read-only package, not in the container's hot core.

What you get

  • Three exporters over one snapshot: toJSON, toDOT, toChromeTrace.
  • fromContainer(c) so you can go straight from a booted container to output: toDOT(fromContainer(c)).
  • Honest opacity: FACTORY and VALUE nodes carry opaqueDeps: true so a reader sees "deps unknown", never "no deps"; ALIAS nodes show their target edge.
  • Deterministic output: keys in a fixed order, arrays in snapshot order, so two runs diff byte-for-byte.
  • Fail-closed everywhere: a malformed snapshot throws a clear, named error rather than emitting a half-valid document.

API reference

Functions

fromContainer(container: { describe(): object }): object
toJSON(snapshot: object): string
toDOT(snapshot: object): string
toChromeTrace(snapshot: object): string
nodeKind(kind: number): string
  • fromContainer -- returns container.describe(). The one container-aware helper; it only calls the PUBLIC describe(). Throws a TypeError if handed anything without a describe method (a container older than 2.1.0, or a non-container), and surfaces the container's own throw if describe() is called before boot.
  • toJSON -- a deterministic, round-trippable JSON string. JSON.parse yields the same node count, same edges, same order. Each node emits its integer kind and a derived kindName; opaque nodes carry opaqueDeps: true; ALIAS nodes carry target. Serializing the same snapshot twice is byte-identical.
  • toDOT -- a Graphviz digraph; each node labelled token + kind name, FACTORY nodes labelled "deps opaque", ALIAS nodes labelled "-> target". Dynamic text is DOT-escaped.
  • toChromeTrace -- a { traceEvents, displayTimeUnit } JSON document for chrome://tracing / ui.perfetto.dev. A DI graph has no wall clock, so this is a documented minimal mapping: each node is a complete ('X') event on a synthetic timeline (ts = teardown-order rank, else node index), each edge a matched flow pair ('s'/'f'). Not a real timeline trace.
  • nodeKind -- map a TYPES integer tag to its label. The ONE place the mapping lives. Throws a TypeError on any tag outside 0..4 (fail closed -- no "unknown" placeholder a consumer might trust).

Kinds

KIND_NAMES is the frozen source-of-truth table; the index is the container's TYPES tag.

| Tag | KIND_NAMES[tag] | Meaning | Notable in output | | --- | ----------------- | -------------------------------- | --------------------------- | | 0 | VALUE | a pre-built value | opaqueDeps, no teardown edge | | 1 | SINGLETON | built once, cached | real dep edges | | 2 | TRANSIENT | built per resolve | real dep edges | | 3 | FACTORY | an opaque factory closure | opaqueDeps, "deps opaque" | | 4 | ALIAS | resolves to another token | target + alias->target edge |

Constants

| Export | Type | Meaning | | ------------ | ------------------- | -------------------------------------------------- | | VERSION | string | Three-place-synced version (1.0.0). | | KIND_NAMES | readonly string[] | Frozen ['VALUE','SINGLETON','TRANSIENT','FACTORY','ALIAS']. |

No default export.

Composability with the container

A full pipeline: wire and boot the container, then render the same snapshot three ways.

import { Container } from '@zakkster/lite-di-container';
import { fromContainer, toJSON, toDOT, toChromeTrace } from '@zakkster/lite-di-graph';
import { writeFileSync } from 'node:fs';

const c = new Container();
c.value('cfg', { tag: 'T' });
c.singleton('svc', class { constructor(cfg) { this.cfg = cfg; } }, ['cfg']);
c.transient('worker', class { constructor(svc) { this.svc = svc; } }, ['svc']);
c.factory('built', () => ({ made: true }));   // opaque deps
c.alias('svcAlias', 'svc');
c.boot();

const snap = fromContainer(c);                 // one snapshot, three views

writeFileSync('graph.json', toJSON(snap));     // round-trippable, diffable
writeFileSync('graph.dot', toDOT(snap));       // dot -Tsvg graph.dot -o graph.svg
writeFileSync('graph.trace.json', toChromeTrace(snap)); // load in ui.perfetto.dev

await c.shutdown();

Zero-GC design notes

Be honest about what this package is: a FORMATTER. It allocates strings by construction -- there is no 0 B/op hot path here and this README does not claim one. What test/torture.mjs gates is that the formatter is LEAK-FREE and BOUNDED:

| Property | Result | How it is gated | | ---------------------------- | --------------------- | ------------------------------------ | | retention (build/format/discard) | size returns to 0 | @zakkster/lite-leak, size 0 | | heap | bounded across cycles | soak, peak stays near baseline | | major GC | none | @zakkster/lite-gc-profiler, maxMajor: 0 | | per-call output | allocates by construction | recorded, NOT gated at zero |

The container's own describe() and get() are untouched by this code -- the exporters are pure over the snapshot argument and never reach into container private state. Numbers reproduce with node --expose-gc test/torture.mjs.

Design decisions worth knowing

  • Pure over the snapshot, not the container. Every exporter takes a { nodes, edges, order } object; fromContainer is the only container-aware helper and it only calls the public describe(). You can format a hand-built snapshot with no container anywhere.
  • Fail closed on a malformed snapshot. A renamed or missing nodes/edges/ order array, a node whose token is not a string/symbol or whose kind is not a valid integer, an edge that is not an object with string/symbol from/to, or an edge referencing an absent token, all throw the same clear, named malformed snapshot error. No half-valid document, no silent coercion.
  • Symbols get distinct IDs but shared labels. Symbol tokens are rendered by their .toString() description in DOT/JSON/trace output. Distinct same-description symbols now get distinct node IDs (so they never merge), but their LABELS read identically -- a display ambiguity inherent to symbols, not a data loss.
  • Opaque deps are surfaced, not hidden. FACTORY and VALUE nodes carry opaqueDeps: true and DOT prints "deps opaque", so an opaque closure is never mistaken for a leaf with no dependencies.
  • Deterministic by construction. Keys are written in a fixed order and arrays preserve snapshot order, so two runs produce byte-identical output you can diff.
  • One source of truth for kinds. The integer -> label mapping lives only in KIND_NAMES/nodeKind; every exporter reads through it, and an out-of-range tag throws rather than inventing an "unknown" label.
  • Not a validator. boot() already validated the graph before a snapshot can exist, so these exporters do not re-check cycles or missing deps -- they format.

Testing

  • npm test -- 17 node:test cases (behavioural coverage), including a fail-closed case per exporter (malformed snapshot throws).
  • npm run torture -- node --expose-gc test/torture.mjs: the retention and bounded-heap gates (leak-free via @zakkster/lite-leak, no major GC via @zakkster/lite-gc-profiler). A formatter allocates by construction, so this gate proves leak-free/bounded, not 0 B/op.
  • npm run example -- examples/export-graph.mjs: a shipped, self-verifying reference consumer. It boots a real container with one of every registration kind (value / singleton / transient / factory / alias, wired with real deps), snapshots it via fromContainer, renders all three formats (toJSON / toDOT / toChromeTrace), and asserts round-trip determinism, the nodeKind / KIND_NAMES mapping, and the fail-closed throw on malformed input -- with node:assert, so a broken contract exits non-zero. It is the downstream proof that the 1.0.0 API works in anger.
  • npm run verify -- all three, in order. prepublishOnly runs verify.

What this is not

  • Not a runtime tracer. There are no per-resolve timings or spans here; for that use @zakkster/lite-trace. toChromeTrace is a static snapshot mapping onto a synthetic timeline, not a recording.
  • Not a validator. A snapshot only exists after a successful boot(), which already checked cycles and missing deps. These exporters format a known-valid graph.
  • Not the container. Wiring, lifetimes, scopes, and teardown live in @zakkster/lite-di-container (the peer dependency).

Ecosystem

  • @zakkster/lite-di-container -- the DI container whose describe() (>=2.1.0) snapshot this package formats (peer dependency).
  • @zakkster/lite-di-event-bus -- a sibling: DI-constructed event fan-out over a multi binding.
  • @zakkster/lite-di-cron -- a sibling: DI-constructed scheduled jobs.
  • @zakkster/lite-gc-profiler / @zakkster/lite-leak -- the GC and retention gates used in the torture tier.

License

MIT (c) Zahary Shinikchiev [email protected]