CLI tool
acrazing/dpdm avatar
acrazing/dpdm

dpdm: a static dependency analyzer that understands TypeScript

Detect circular dependencies in your TypeScript projects.

912 stars24 forksTypeScriptMIT

At a glance

What is it?
dpdm analyses the import graph of a JavaScript or TypeScript project to find circular dependencies, and it is built on the TypeScript parser so it resolves path mapping, package-local tsconfig files and project references. It can fail a build with a non-zero exit code, which is what makes it a check rather than a report.
Who is it for?
dpdm suits a TypeScript team that wants circular dependency detection wired into CI, especially one with path aliases, project references or a monorepo layout where a naive import scanner would report false cycles. It does not suit a project that expects runtime dependency analysis, since dpdm works statically on source imports, and it does not replace a type checker, because it can be told to ignore type dependencies entirely.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 86 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the tool actually computes

dpdm is a static dependency analyzer for JavaScript and TypeScript projects. The repository description is narrower still: detect circular dependencies in your TypeScript projects.

The computation is a graph. You give it entry files, it resolves what each file imports, follows those imports, and reports the shape of the resulting graph, including the cycles in it. The output has three parts, each of which can be turned on or off: a tree of the dependencies, the circular dependencies themselves, and warnings.

The interesting part is the resolution step. dpdm parses every module with the TypeScript compiler rather than with regular expressions over import statements, and it loads modules asynchronously. That choice is what makes the TypeScript-specific features work: path mapping, package-local tsconfig.json files, TypeScript project references, and the ability to ignore type-only dependencies. A scanner that reads import lines cannot know that an import exists only to carry a type, and cannot know that @/components/Button resolves to a file two directories away through a tsconfig paths entry.

The audience is a front-end or full-stack team with a TypeScript codebase that has grown dependencies organically. Circular imports are not always a bug, but they frequently signal a module boundary that is in the wrong place, and they cause real problems at runtime with partially initialised modules.

TypeScript resolution details that change the result

The resolution features are listed as highlights, and they are the reason to choose this tool over a simpler scanner.

Path mapping means the paths aliases in your tsconfig, the @/ style shortcuts, are resolved to real files, so a dependency that looks like an import of a package is understood as an import of your own source file. Package-local tsconfig.json files are supported, which matters in a monorepo where each package carries its own configuration. Project references are supported as well, and the --tsconfig option documents a fallback behaviour: the default is tsconfig.json if it exists in the context directory, project references are supported, and when the option is omitted dpdm can use the nearest package tsconfig.json for each source file.

Type dependencies can be ignored, either with the -T or --transform flag, which transforms TypeScript modules to JavaScript before the analysis and therefore omits type-only dependencies from the graph. That is a real trade-off rather than a free win. Type-only imports are erased at compile time, so they cannot cause a runtime cycle, and including them makes the report noisier. But a cycle that exists only in the type layer is often the design smell you wanted to see, and turning the flag on hides it.

CommonJS and ESM are both supported, and JavaScript and TypeScript are supported completely, with the default extension list covering .ts, .tsx, .mjs, .js, .jsx and .json.

Turn the finding into a build failure

The feature that matters for continuous integration is the exit code option. The documentation for it says the value format is CASE:CODE, that circular is the only supported CASE, and that CODE should be an integer between 0 and 128. The example given is dpdm --exit-code circular:1, and the program will exit with code 1 if a circular dependency is found.

That single flag is what turns dpdm from a report into a gate. A dependency cycle that appears in a pull request can fail the pipeline the same day, before it becomes the reason a module is undefined at load time in production.

The other flags control what you see while you are still working on it. --no-tree and --no-warning suppress the tree and the warning output so that only the cycles are printed, and the README shows that combination as the way to print circular dependencies only. --no-circular turns the cycle report off, which pairs with --detect-unused-files-from for the other job dpdm does: finding files nothing imports.

That last one is worth pulling out, because it is a different feature hiding behind the same tool. The example finds unused files by index.js in the src directory, and the flag takes a glob and a set of entry points. A dependency graph can answer which files are unreachable from the entry points, and that question is often more useful than the cycle question on a project that has never been analysed.

For a monorepo there is --group-by-package, which shows dependencies and circular dependencies grouped by package rather than by file, which is the view that tells you which workspace boundary is leaking.

Installing and the first run

There are two installation shapes, and the difference is whether the tool is a command on your machine or a dependency of your project.

For command line use:

bash
npm i -g dpdm
# or via yarn
yarn global add dpdm

As a module in your project, which is the shape that belongs in a CI pipeline:

bash
npm i -D dpdm
# or via yarn
yarn add -D dpdm

The simplest invocation takes an entry file or a glob:

bash
dpdm ./src/index.ts

That prints the dependency tree, the circular dependencies and the warnings for everything reachable from that entry point. The example directory in the repository contains circular_a.js through circular_e.js plus index.js and index_lib.js and a node_modules directory, which is a small fixture set for exercising the cycle detection, and the package.json has a demo script that runs the CLI over ./example/index.js through ts-node.

Two working-directory options matter when the tool runs in CI. --cwd sets the working directory used to match files and resolve relative paths, and the README shows dpdm --cwd ../other-project ./src/index.ts for analysing files from another directory. --context sets the context directory used to shorten paths in the output, and the default is the current directory.

The package itself ships CommonJS and ESM builds, with main pointing at lib/index.js, module at lib/index.mjs and types at lib/index.d.ts, and the binary is registered as dpdm in the bin field.

Config file and the ignore escape hatches

The configuration file is optional and the CLI options override it. dpdm searches for dpdm.config.ts, dpdm.config.mts, dpdm.config.cts, dpdm.config.mjs, dpdm.config.cjs, dpdm.config.js or dpdm.config.json in the working directory, and the --config option points at a path explicitly.

The configuration is written in TypeScript with a defineConfig helper:

typescript
// dpdm.config.ts
import { defineConfig } from 'dpdm';

export default defineConfig({
  files: ['./src/index.ts'],
  exitCode: 'circular:1',
  transform: true,
  warning: false,
});

Once the file exists, the CLI is invoked with no arguments at all:

bash
dpdm

The ignore mechanisms are where most real projects will need to spend time, and there are three of them.

The first is a source comment. The @dpdm-ignore comment can be placed before an import, export, require() or dynamic import() dependency:

typescript
// @dpdm-ignore
import './intentional-cycle';

The second is the --skip-imports option, which takes patterns to ignore when finding circular dependencies, with the example form dpdm ./src/index.js --skip-imports 'src/a.js:.*' src/c.js:src/d.js. The third is --skip-dynamic-imports, whose value can be circular to ignore dynamic imports only when parsing circular references, or tree to ignore them when parsing source files.

And then there is the warning filter, --ignore-miss-warning and --ignore-skip-warning, for suppressing the two categories of message that come from files that could not be resolved or were skipped. The example shows --ignore-miss-warning '^vscode$' --ignore-skip-warning '^node_modules/'. These exist because the analyzer reports what it could not resolve, which is exactly the information you do not want to drown in when the missing module is an editor shim.

Where dpdm differs from madge, and where it stops

The README states the stable output claim directly: this is compared to madge, whose results are completely inconclusive when analyzing TypeScript. That is a strong claim from the author of the competing tool's weakness, so it is worth separating the mechanism from the claim. The mechanism is that dpdm parses modules with the TypeScript compiler, so it applies the same resolution rules the compiler will apply, including path mapping and type-only import elision. A tool that greps import statements does not.

The honest limits are of a different kind. dpdm is static, so it sees what the source says, not what the program does at run time. Conditional imports behind a platform check, dynamic import() with a computed specifier, and code that depends on module resolution at runtime are all cases where the graph dpdm computes is an approximation. That is also why the ignore options exist in the numbers they do: a project with legitimate runtime-resolved imports will need them, and each ignore is a small hole in the graph.

The other limit is scope. dpdm reports dependency structure, not correctness. A graph with no cycles can still be badly layered, and a cycle that is genuinely fine, a type-only mutual reference between two modules that never run at the same time, will still be reported unless you configure it away.

For maintenance, the repository is not archived and the last push was on 2026-07-29, with releases v4.3.0 on 2026-07-29, v4.2.0 and v4.1.0 both on 2026-05-09. The licence field on the repository says MIT, and the LICENSE file is at the top of the tree. The project uses vitest for tests, husky for git hooks, and .ncurc.json for dependency update automation.

Editorial conclusion

dpdm suits a TypeScript team that wants circular dependency detection wired into CI, especially one with path aliases, project references or a monorepo layout where a naive import scanner would report false cycles. It does not suit a project that expects runtime dependency analysis, since dpdm works statically on source imports, and it does not replace a type checker, because it can be told to ignore type dependencies entirely. Before adopting, run dpdm against your entry file with the tree output and read the warnings, because a project that has never looked at its import graph will usually turn up more than it expects, and decide deliberately whether to use --transform before you turn on --exit-code circular:1 in CI.

Frequently asked questions

What is dpdm and what does it detect?

dpdm is a static dependency analyzer for JavaScript and TypeScript projects. It resolves the import graph from given entry files and reports the dependency tree, the circular dependencies in it and warnings about files it could not resolve.

How do I make dpdm fail the build when a cycle is found?

Use the --exit-code option with the form CASE:CODE, where circular is the only supported case and the code is an integer between 0 and 128. The example dpdm --exit-code circular:1 exits with code 1 when a circular dependency is found.

How do I install dpdm as a project dependency?

Run npm i -D dpdm, or yarn add -D dpdm, to add it to the dev dependencies of your project. For command line use across projects, the README shows npm i -g dpdm or yarn global add dpdm.

Does dpdm understand tsconfig path mapping and project references?

Yes. The README lists TypeScript path mapping, package-local tsconfig.json files and TypeScript project references as supported, and the --tsconfig option documents that when it is omitted dpdm can use the nearest package tsconfig.json for each source file.

How do I ignore an intentional circular import in dpdm?

Place an @dpdm-ignore comment before the import, export, require() or dynamic import() statement. There are also the --skip-imports option for patterns to ignore while finding cycles, and --skip-dynamic-imports for dynamic imports.

Official sources

  1. acrazing/dpdm on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/acrazing-dpdm.svg)](https://hysenlabs.com/projects/acrazing-dpdm)

Was this useful?