How to measure an npm package's blast radius before publishing
A practical multi-repo workflow for finding consumers, reconstructing real dependency paths, and accounting for lockfiles, local links, and Git state.
In this article10 sections
Suppose you have changed @acme/design-tokens. Its tests pass and the next version is ready. One apparently simple question remains: which projects should you verify before publishing?
In a monorepo, a build graph and CI often provide the answer. Across separate repositories, the release check turns into an investigation: search every package.json, inspect lockfiles, resolve local links, switch branches, and remember which project is actually using which version.
This article turns that investigation into a repeatable workflow. Even if you never automate it, you will end up with a useful checklist and a report format that explains every conclusion.
Finding a name is not the same as measuring impact
Search answers “Where does this package name occur?” Impact analysis has to answer four different questions:
- Which projects are direct and transitive consumers?
- Which version is requested, and which version is actually resolved?
- Is a workspace, link, or local path overriding registry resolution?
- What Git state is each consumer in right now?
A compact mental model is:
impact = consumers × dependency paths × resolved versions × repository state
This is not a formula for calculating a score. It is a reminder that a conclusion becomes unreliable when any one dimension is missing.
For example, a text search will not find design-tokens in the application manifest when the path is:
storefront
└── @acme/ui-kit
└── @acme/design-tokens
The token change still reaches storefront. The unit of analysis therefore has to be a path from a project to the changed package, not a matching line in a file.
Define the release question first
Before walking repositories, record:
- the package name and current version;
- the intended next version or range;
- the kind of change: API, runtime behavior, styles, types, or build output;
- the root directories containing potential consumers;
- the package manager and registry, if it is not the public npm registry.
SemVer helps a consumer decide whether a version satisfies a range. It does not prove that a particular change is compatible with a particular codebase. A patch release can still alter visual snapshots, generated types, or bundler assumptions.
Step 1: use manifests to build a candidate list
A first pass can be plain search:
PACKAGE_NAME='@acme/design-tokens'
PROJECTS_ROOT='/path/to/projects'
rg --fixed-strings "\"$PACKAGE_NAME\"" "$PROJECTS_ROOT" \
--glob 'package.json' \
--glob '!**/node_modules/**'
The output is useful but incomplete. It finds declarations in dependencies, devDependencies, peerDependencies, and related fields; it does not reconstruct transitive paths.
Transitive dependencies are packages a project depends on indirectly, through one or more other dependencies.
Capture at least these fields for each candidate:
| Field | Why it matters |
|---|---|
| Project | The concrete repository or workspace |
| Requested | The range declared in the manifest |
| Relationship | direct, dev, peer, or optional |
| Path | The chain to the changed package |
| Evidence | manifest, lockfile, or local link |
Do not decide what to update yet. Build the graph first, or the first direct matches will look more important than hidden transitive paths.
Step 2: compare declared and locked versions
A manifest expresses intent; a lockfile records the selected installation state. npm describes package-lock.json as a concrete dependency tree that supports reproducible installs: package-lock.json documentation.
The locked version may differ from what is currently installed: dependencies may not have been reinstalled after switching branches. Also, npm ci --omit=dev keeps development dependencies in the lockfile without installing them on disk. If you need to know which code the project actually uses, check the installed tree separately. See the npm documentation on omitting dependencies during installation.
In practice, every path needs two values:
requested, such as^2.1.0;resolved, such as2.3.4.
They answer different questions. A range may already allow the next release while the current lockfile continues to pin an older one. Conversely, several packages may request compatible ranges while peer-dependency rules produce multiple instances in the installed tree.
A useful report might look like this:
| Project | Requested | Resolved | Path | Conclusion |
|---|---|---|---|---|
| admin-app | ^2.1.0 |
2.3.4 |
direct | verify the lockfile update |
| storefront | — | 2.2.0 |
via ui-kit |
inspect the intermediate package first |
| docs | workspace:* |
local | linked | test the local source |
Searching a lockfile for a package name is also insufficient. The file may contain several variants or entries no longer reachable from the project root. You need to traverse relationships from the root project, not count matching strings.
Step 3: inspect local resolution separately
A local dependency can change the meaning of the entire result. The manifest may resemble a registry dependency while the project actually resolves through a workspace, file:, link:, portal:, Yarn link, or an ordinary symlink.
Package managers model these mechanisms differently. Useful references include Yarn’s link: and portal: protocols, plus the pnpm workspace protocol.
For every local relationship, verify:
- The canonical target after resolving symlinks.
- Whether the target
package.jsonis the package you intend to publish. - Whether the link is intentional rather than package-manager store plumbing.
- Which branch is checked out in the linked repository.
That last check matters. A local link can silently include uncommitted changes and create confidence that will disappear when a clean registry version is installed.
Step 4: add Git state to the graph
A project list without branches becomes stale quickly. The same repository may be open on a feature branch, exist in several worktrees, or contain uncommitted changes.
The minimum check for each consumer is:
git -C "$PROJECT_DIR" status --short --branch
git -C "$PROJECT_DIR" worktree list
Git worktrees are first-class working trees for one repository, not merely duplicate folders: git-worktree documentation.
Add these fields to the report:
- active branch;
- dirty or clean status;
- main checkout or linked worktree;
- whether the branch represents the scenario you intend to test.
If a project is on an unexpected branch, its analysis should not be treated as evidence about main.
Step 5: turn the map into an action plan
The projects can now be classified:
| Signal | Priority | Next action |
|---|---|---|
| Direct dependency with an incompatible range | high | update the range, install, run checks |
| Transitive path through an internal library | high | verify and release the intermediate library first |
| Locally linked package | medium | repeat against a clean checkout and registry version |
| Path found only in a lockfile | investigate | confirm that the node is reachable from the project root |
| Unexpected branch or dirty status | blocking | return the repository to a known state |
A good report does not merely list consumers. It explains why each project is present and what should happen next.
A minimal repeatable workflow
The complete process fits into seven steps:
- Record the package, version, and kind of change.
- Find direct candidates in manifests.
- Build transitive paths across local packages.
- Compare requested and resolved versions from lockfiles.
- Identify workspaces, links, and canonical filesystem paths.
- Add branch, worktree, and dirty status.
- Produce an update order and validation commands.
For two or three repositories, doing this manually is reasonable. Keep the result in a table and refresh it before each shared-package release.
When automation becomes necessary
Automation becomes worthwhile because of intersections, not because one package.json is large:
- several roots containing repositories;
- npm, Yarn, and pnpm in one environment (uncommon, but possible);
- direct, transitive, and lockfile-only paths;
- local relationships (symlinks) and workspaces;
- parallel branches and worktrees;
- repeated actions across several consumers.
Eventually, another script dump is no longer the useful artifact. The useful artifact is an interactive map where the project, path, version, branch, and next action share one row.
That is when I moved this workflow into the IDE I use every day. The result was Dependency Radar for WebStorm: it starts with the open package, scans configured roots, combines manifests, lockfiles, and local relationships, adds Git context, and lets you move from a discovered path to an action.
The plugin does not replace tests, review, or the SemVer decision. It automates evidence collection before those decisions. In a small monorepo, that may be unnecessary. In a multi-repo environment or a microservices architecture, it keeps you from repeating the same investigation before every release or feature change.
Pre-publish checklist
Before running publish, make sure you can answer yes to five questions:
- Have direct and transitive consumers been found?
- Are requested and resolved versions known for every path?
- Do local links and workspaces resolve to the expected project?
- Are branches, worktrees, and uncommitted changes accounted for?
- Is there an update order and validation command for every affected project?
The goal is not to build the prettiest graph. The goal is to know which evidence supports the release decision and what the next step is for every consumer.
Disclosure: an AI system was used to help write and edit this article.