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

deblob

v0.0.6

Published

Machine-checkable hexagonal architecture for TypeScript/ESM. CLI: check (dag, layers, private, barrels, ports) and explain.

Downloads

405

Readme

deblob

Machine-checkable hexagonal architecture for TypeScript/ESM: layer matrix, composition rules, visibility boundaries — checked in CI, without an agent.

deblob detects; it never moves code. Moving code into layers is judgment, and judgment stays with you (or your agent). What the tool gives you is a mechanical guarantee over the code that opts in: labeled files honor their layer's constraints, or CI says exactly which rule broke and why. Unlabeled code is blob — legal, unchecked; labeling is adoption, not a prerequisite.

Commands

deblob                       project status + discovery
deblob check [what...]       run architecture checks (default: all)
deblob explain <topic...>    explain rules or checks (service-purity,
                             layers, ...)
  • deblob prints the inventory — file count, total size, blob % (size-weighted, hence the size in the headline), then service count and, for a package declaring the deblob field, its exports claim as written (exports 9 claimed, 2 disclosed) — plus where to go next. Informational by contract: always exits 0, so a stray run can never fail a build.
  • deblob check is the gate. Checks: dag (service cycles over every import kind, runtime module cycles — no-service-cycle, no-runtime-cycle), layers (the dependency matrix — inward-deps, service-purity, blob-quarantine, service-assembly-only, adapter-assembly-only, runtime-import, public-unit), private (private-sealed), barrels (layer-in-path), ports (ports-types-only), surface (the exports map matches the layers it fronts — only for packages declaring "deblob": {} in package.json; layer-in-path, chain-purity). All run over one shared import graph. The summary is two lines: the verdict with the inventory (0 violations · 58 files · 300kb · 10% blob), then coverage (4 services · 181 imports · exports 7 checked, 2 disclosed) — the exports segment exists only when a claim was checked, so a field dropped in a merge is a visible diff in the CI log, not a gate that went quiet. Naming surface by hand on a package with no field prints one stderr line saying nothing was checked. Exit codes: 0 clean, 1 violations found, 2 usage or config error — and the two uncertifiable runs: an import that did not resolve, an exports entry surface could not reach.
  • deblob explain service-purity prints the rule's rationale and the shipped knowledge card — offline, version-matched with the binary. Several topics at once work too: the check footer prints the fired rules as a pasteable deblob explain service-purity private-sealed no-service-cycle. deblob check --explain appends the explanation of every rule that fired; a CI log becomes self-teaching in one run.

Violations cite their rule and print the offending edge:

src/invoice
  src/invoice/pdf-render.service.ts
    layers   imports node:fs — service layer cannot depend on concrete;
             import type is fine (service-purity, runtime-import)

Not in v0, on purpose: autofix (not deblob's job — fixing belongs to whoever holds the context, agent or human; a gate that ships its own fixes grades its own homework, and its green stops being evidence), --json/--sarif (staged refinement).

Configuration

Optional. No config file at all resolves to honest defaults: the stock ts-suffixes-factories flavor, whole-tree coverage. When you need one, deblob.config.ts at the project root (TS loads natively — Node ≥ 22.18, erasable syntax only; under a "type": "commonjs" package, npm's default, name it deblob.config.mts):

import { defineConfig } from "deblob"

export default defineConfig({
  include: ["src/**"],
  assembly: ["src/main.ts"],
  pure: ["zod"],
})

The eleven keys, all optional:

| Key | Default | Meaning | | ---------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | flavor | "ts-suffixes-factories" | Architecture style — a stock name, or a custom FlavorResolver exported from the config | | assembly | [] | Globs designating composition roots — privilege is declared, not presumed | | include | ["**"] | Coverage globs; under-coverage is a silent hole, so the default covers everything | | exclude | [] | Appended to a non-removable baseline (node_modules, dist, …); never replaces it | | pure | [] | service-purity allowlist: package names, builtin specifiers, and declared external patterns ratified as pure | | typeOnlyExempt | flavor's stance (true) | false = strict: type-only imports lose their exemption under runtime-import; knobs only tighten canon | | tsconfig | tsconfig.json at root | The tsconfig feeding resolution (paths aliases); a path, or false to disable — a declared path that doesn't exist fails loud | | alias | {} | Resolver aliases living outside tsconfig (bundler config); teaches resolution, never suppresses failures | | external | [] | Specifier patterns the environment provides with nothing on disk ($theme:**, cloudflare:*) — matches are leaves, never resolved | | externalLayers | {} | Specifier pattern → layer: cross-package identity declared by hand; wins over a producer's deblob field — blob revokes a claim | | build | "dist" | The output directory mirroring src/ one-to-one, so surface reaches source through built exports; { mirror: {…} } for several |

Discovery walks upward from cwd; the nearest config wins and its directory becomes the project root. No merging, no inheritance. -c/--config <path> overrides the walk.

A declared pure entry is trusted, not verified — the guarantee is only as good as the config review. Unlisted third-party imported from a pure layer fires as unclassified: one config line fixes a false positive; the reverse default would be a silent hole.

An import that fails to resolve fails the run: check exits 2 — not 1, because the fault may be the run's world (unwired tsconfig, missing install, bundler-only alias, environment-provided module) rather than the code — and lists each offender with the remedies. A green check thereby certifies a complete graph. Non-literal dynamic imports (import(expr)) are exempt: unresolvable by construction, never a missing edge.

A module the environment provides with nothing on disk — a vite plugin serving $theme/config, a runtime exposing cloudflare:workers, a npm: or URL specifier — is declared, not aliased: external holds patterns over the specifier as written, and a match is a known leaf. Resolvable packages need no entry. A declared external counts concrete by default; to ratify it pure, list the same pattern in pure — the pattern is the leaf's identity, matched verbatim like a package name. Patterns are not path globs (a specifier is one string): ** matches any characters, / included — $theme:** is the whole namespace — and * matches anything but /. Not covered yet: teaching the resolver a bundler exports condition (browser, svelte); until a conditions key exists, external is the workaround.

Across package boundaries, layer identity travels while everything else stays sealed: each package's gate covers its own interior, and a package declaring "deblob": {} in its package.json claims that the stock naming rule holds on its exports surface — @repo/billing/checkout.service is a service, sealed to assembly for every consumer; @repo/billing/totals.model is a model, pure for your model layer with no pure line. Flavors classify locally; layers travel: the consumer reads results, never the producer's machinery, and only for subpaths the producer's exports map lists — an import that reaches around the map is unlabeled, as any deep import. The claim is trusted the way the code is — you already run it and trust its versioning; a stale field is a stale semver, no worse — and externalLayers is the override, blob the word that revokes a claim you do not buy. For packages that declare nothing, externalLayers patches identity by hand, and an unlabeled external behaves exactly as before — nothing is demanded from anyone who does not opt in.

The field is a checked claim, not marketing. The producer's own surface check resolves every exports entry to a source module — source paths directly, built paths through the build mirror (dist/index.js → src/index.ts, extensions stripped, exact match only; no build needs to exist on disk) — and verifies the subpath's naming against the file's layer, following re-export chains so an unlabeled root cannot front a service. A pattern entry ("./*": "./dist/*.js") expands the way Node resolves it, over your source: every module the star can bind is judged under the subpath it gets. The mirror is your promise that the build is one-to-one, not something the tool measures; a bundled package has no honest mirror and says build: false. What the mirror cannot reach is not guessed and not judged: the run reports the entry as unverified, names the two remedies, and exits 2 — no green until it is mapped, or disclosed. Disclosure is public: "deblob": { "blob": ["./legacy/**"] } lists the subpaths the field does not cover; they classify as unlabeled abroad, exactly as if the package had no field, and the claim reads precisely — every subpath not listed is verified at the producer's gate. The field's other word is assembly: "deblob": { "assembly": ["./cli"] } designates a published composition root as wiring — sealed to every consumer's own wiring, exactly as an in-set assembly file is — and, like an in-set designation, it is never verified: a carve-out at home, a seal abroad. The two words are the producer's choice of meaning for an unsuffixed entry: blob says "consume it like any third-party package", assembly says "import it from wiring only".

Why each rule exists

deblob explain <rule> ships the answer with the binary. The full theory: docs/architecture.md.