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

@conmoong/graph-validate

v0.1.0

Published

Workspace-level checker for @conmoong/graph's module-graph facts: cycles, phantom/dev-only dependencies, unused files and packages, and import boundary rules, combined across a tsconfig "references" tree or an explicit set of .graph.json files.

Readme

@conmoong/graph-validate

Workspace-level checker for @conmoong/graph's module-graph facts. @conmoong/graph (tsc-p's compiled-in emit plugin) only ever records facts — it never judges them once "emit" is configured. This package is the separate tool that reads those facts and applies rules, either for one project or combined across a tsconfig "references" tree.

The .graph.json fact schema

This is the file @conmoong/graph writes when its "emit" option is configured (default name <tsconfig-basename>.graph.json, next to the tsconfig). It is facts only — no rule configuration, no severities, no pass/fail judgements — so it doubles as a general-purpose resource (e.g. a future bundler tree-shaking input), not just this tool's own input.

{
    // The nearest package.json's own "name" field, if any. "" if none was found.
    "package": "my-package",

    // The absolute path this file was written to (informational).
    "tsconfig": "/abs/path/to/tsconfig.graph.json",

    // Every source file this compilation processed, relative to the
    // tsconfig's own directory, always "./"- or "../"-prefixed.
    "files": ["./src/index.ts", "./src/helpers.ts"],

    // Every named/default export, one entry per (file, name).
    "exports": [
        { "file": "./src/helpers.ts", "name": "helper" }
    ],

    // Every import/export-from/require()/dynamic-import() reference.
    "edges": [
        {
            "from": "./src/index.ts",
            // Present only for an "internal" edge: the resolved project file.
            "to": "./src/helpers.ts",
            // The specifier exactly as written in source.
            "specifier": "./helpers",
            // "internal" (resolves to another file in this compilation) or
            // "external" (resolves to node_modules, a workspace sibling
            // package, or could not be resolved at all).
            "kind": "internal",
            // Present only for an "external" edge that DID resolve: the
            // resolver's own resolved package identity (module.ResolvedModule.PackageId.Name),
            // NOT the raw specifier text — "@babel/core/lib/foo" and
            // "@babel/core" both record resolvedPackage "@babel/core".
            "resolvedPackage": "",
            // The specific named bindings reached, if any were written
            // (e.g. `import { helper }`) — absent for a bare/default/
            // namespace-only import or an unnamed require()/dynamic import().
            "names": ["helper"],
            // `import def from "x"` / `const def = require("x")` (whole binding, no destructure).
            "isDefault": false,
            // `import * as ns from "x"` — conservatively treated elsewhere
            // as "could reach anything x exports".
            "isNamespace": false
        }
    ]
}

Known v1 simplifications (see internal/tscp/graph/transformer.go's own comments for the authoritative list): destructuring export patterns (export const { a, b } = obj) and export = expr are not modelled as named exports; a require()/dynamic import() never records names even when the caller immediately destructures its result.

The @conmoong/graph plugin config (for context)

Rules live in the SAME tsconfig @conmoong/graph plugin entry that controls "emit" — they can be present together; "emit" always wins for evaluation (this tool is what applies "rules" in that case), and tsc-p itself only evaluates "rules" inline when "emit" is not set (see the root README's @conmoong/graph section for the full plugin design and inline-evaluation behaviour):

{
    "compilerOptions": {
        "plugins": [{
            "name": "@conmoong/graph",
            "emit": true,
            "rules": [
                { "module": "some_npm_library", "phantomImport": "error" },
                { "module": "./*", "cycle": "error" },
                { "module": "./src/*", "devLeak": "error" },
                { "module": "./src/features/*", "unused": "error" },
                { "module": "./src/features/*", "importRules": [
                    { "module": "./src/other-features/*", "severity": "error" }
                ] }
            ]
        }]
    }
}

A "module" pattern uses the same single-* wildcard syntax as tsconfig "paths". A pattern starting with "./" or "../" is a file pattern; anything else (a bare name or scoped package name, e.g. "@babel/*") is a resolved package name pattern — this exact distinction is also how graph-validate decides whether a sub-project's rule needs re-scoping when merged into a references tree (see below). The most-specific matching pattern wins (an exact pattern always beats a * pattern; among * patterns, the longer prefix wins); entries sharing the exact same pattern text merge, with a later entry's fields overriding an earlier one's.

Checks: cycle (import cycle), phantomImport (a resolved package used but not declared in package.json dependencies), devLeak (used at runtime but only declared in devDependencies — complementary with phantomImport, not mutually exclusive), unused (this file, or this declared dependency, is never imported anywhere in the graph — a file's own entry point should be given an explicit "allow" override, since nothing internal ever imports it), importRules (a boundary/layering list on an entry's own pattern: {"module": pattern, "severity"}, default "allow" — nothing is denied unless listed, and a target can be either a file or an external package).

CLI usage

npx graph-validate -p tsconfig.json      # walk "references" transitively
npx graph-validate -c graph-validate.json # an explicit file list instead
npx graph-validate                        # tries ./graph-validate.json, then ./tsconfig.json

-p <tsconfig>: walking a references tree

Starting from the given tsconfig, references is walked transitively (needed for multi-hop cross-package cycles, not just direct references). For each tsconfig visited (the root included):

  • If its @conmoong/graph plugin entry has "emit" configured, the resulting .graph.json path is required — missing is a hard error ("tsc-p was not run properly, run it first"), since the project explicitly promised a facts file.
  • Otherwise, the conventional <tsconfig-basename>.graph.json path is tried opportunistically (tolerating a manually- or externally-produced file); missing there is only a warning, and that one project is skipped — not fatal to the rest of the run.

Rule combination: the effective rule set is the combination of every visited project's own rules (from its own @conmoong/graph plugin entry) plus the root tsconfig's own rules — not the root's alone. A sub-project's own file-pattern rules are first re-scoped into a shared, root-relative namespace (a rule written as "./src/*" inside packages/foo/tsconfig.json is merged as "./packages/foo/src/*"), so it can never accidentally match a sibling package's files. On an exact pattern-text collision after re-scoping, the sub-project's own rule wins over the root's (the root's rules act as workspace-wide defaults a project can override). Package-name patterns (not starting with "./" or "../") are never re-scoped.

Cross-package cycles are detected even though a fact only records "this file imports resolved package X", never which specific file inside X — graph-validate recognises when an external edge's resolvedPackage matches another loaded project's own "package" field and treats that whole package as one traversable unit for cycle purposes. phantomImport/ devLeak/unused(dependency)/importRules remain evaluated per project, against that project's own package.json. A file is never flagged unused if some other loaded project's graph imports its whole package — facts don't record which specific file satisfies a cross-package import, so the whole package is conservatively treated as possibly used.

-c <config.json>

{
    // Glob(s) matching .graph.json files directly (NOT tsconfig files),
    // relative to this config's own directory — behaves like a
    // tsconfig's own "include" field.
    "included": ["packages/*/tsconfig.graph.json"],
    "rules": [ /* same schema as a plugin entry's "rules" */ ]
}

No references walking or per-project rules combination happens in this mode — rules here is the only rule set applied, uniformly, to whatever .graph.json files match included. Each matched project's own directory (where its .graph.json lives) is still used to look up its nearest package.json for phantomImport/devLeak/unused.

Known v1 limitations

extends is not followed when reading a tsconfig — "plugins" and "references" are read directly off each tsconfig as written. JSONC comments/trailing commas are stripped with a pragmatic string pass, not a full parser.