Skip to Content
Graph CLI

This page documents the 0.4 release candidates of @nexusdi/core. The newest on npm is 0.4.0-rc.1. The documentation for 0.3.2, the current release, is at nexus.js.org. Try the release candidate.

Visualizing Your Dependency Graph

Understanding the architecture of a large application is easier when you can see it. The @nexusdi/cli package provides a command-line tool that analyzes your root module and draws the resulting dependency graph.

Installation

To use the graph tool, install the CLI and the devtools plugin as development dependencies:

npm install -D @nexusdi/cli@next @nexusdi/devtools@next

@nexusdi/cli and @nexusdi/devtools must have the same version as the project’s @nexusdi/core. The command uses the project’s own @nexusdi/devtools, and through it the project’s @nexusdi/core.

Drawing the Graph

The nexusdi graph command takes an entry point (the file defining your root module) and optionally a specific export. The entry is written path#export: the file and the name it exports. With no #export it takes the default export.

npx nexusdi graph src/meridian.module.ts#Meridian

The command imports the entry file and checks the graph with inspect() from @nexusdi/devtools, which runs no constructor. Importing the file still runs its top-level code, so a file that starts the app starts it. Point the command at the file that defines the root module, and keep the Nexus.create call that starts the app in another file, such as main.ts.

The default output is Mermaid, which GitHub and GitLab draw with nothing installed. toMermaid from @nexusdi/devtools writes the same text from a graph in your own code:

import { Token, defineModule, provide } from '@nexusdi/core'; import { inspect, toMermaid } from '@nexusdi/devtools'; interface IReactorCore { readonly output: number; } interface IShipComputer { status(): string; } const REACTOR = new Token<IReactorCore>('ReactorCore'); const COMPUTER = new Token<IShipComputer>('ShipComputer'); class FusionReactor implements IReactorCore { readonly output = 1.21; } class QuantumComputer implements IShipComputer { static deps = [REACTOR] as const; private readonly reactor: IReactorCore; constructor(reactor: IReactorCore) { this.reactor = reactor; } status() { return `online at ${this.reactor.output} GW`; } } const Engineering = defineModule({ name: 'Engineering', providers: [ provide(REACTOR, { useClass: FusionReactor }), provide(COMPUTER, { useClass: QuantumComputer }), ], exports: [COMPUTER], }); const Meridian = defineModule({ name: 'Meridian', imports: [Engineering] }); const mermaid = toMermaid(inspect(Meridian)); const lines = mermaid.split('\n'); // -> ['flowchart LR', ' subgraph m1["Engineering"]', ' p0["ReactorCore<br/>FusionReactor"]', ' p1["ShipComputer<br/>QuantumComputer"]', ' end', ' p1 --> p0', ' classDef exported stroke-width:3px', ' class p1 exported', ''] console.log(mermaid);

Each provider shows its token and, where the names differ, the class bound to it. An exported provider has a heavy border.

Try it:

  1. Save the region as src/meridian.module.ts, replace its last three lines with export { Engineering, Meridian };, and run npx nexusdi graph src/meridian.module.ts#Meridian.
  2. Delete the provide(REACTOR, { useClass: FusionReactor }) line and run the command again. The graph is invalid, so the command prints the NEXUS_BLUEPRINT_INVALID text to stderr and exits with code 1. The first line is nexusdi: [NEXUS_BLUEPRINT_INVALID] the module graph has 1 error; nothing was built., and under it NEXUS_MISSING_PROVIDER says ShipComputer in module Engineering depends on ReactorCore with no visible provider.
  3. Put the provider back and run the command again. It prints the Mermaid text and exits with code 0.

A .ts entry needs no build. When the project has tsx installed, the command loads the entry through it with the project’s tsconfig.json. Without tsx, Node 22.18 and later load TypeScript that only needs its types removed and whose imports name .ts files.

Export Formats and Views

You can customize the output and the level of detail:

  • Formats: Use -f or --format to choose between mermaid (default), dot, json, svg, or png.
  • Output file: Use -o or --out to write to a file. Without -f, the command infers the format from the file’s extension (.mmd, .dot, .json, .svg, .png). With neither, it writes Mermaid to stdout.
  • Views: Use --view modules to see the high-level module import graph without the individual providers. --view defaults to providers, and the modules view has one node per module.
npx nexusdi graph src/meridian.module.ts#Meridian -o graph.svg --view modules npx nexusdi graph src/meridian.module.ts#Meridian -o graph.mmd npx nexusdi graph src/meridian.module.ts#Meridian --view modules -f dot
import { Token, defineModule, provide } from '@nexusdi/core'; import { inspect, toMermaid } from '@nexusdi/devtools'; interface IReactorCore { readonly output: number; } const REACTOR = new Token<IReactorCore>('ReactorCore'); class FusionReactor implements IReactorCore { readonly output = 1.21; } const Engineering = defineModule({ name: 'Engineering', providers: [provide(REACTOR, { useClass: FusionReactor })], exports: [REACTOR], }); const Meridian = defineModule({ name: 'Meridian', imports: [Engineering] }); const modules = toMermaid(inspect(Meridian), { view: 'modules' }); const lines = modules.split('\n'); // -> ['flowchart LR', ' m0["Meridian<br/>0 providers"]', ' m1["Engineering<br/>1 provider"]', ' m0 --> m1', ''] console.log(modules);

SVG needs @viz-js/viz, and PNG also needs @resvg/resvg-js. Mermaid, DOT and JSON need nothing else:

npm install -D @viz-js/viz @resvg/resvg-js npx nexusdi graph src/meridian.module.ts#Meridian -o graph.svg

@viz-js/viz runs Graphviz as WebAssembly. A format whose package is missing exits with code 3 and prints one install line, such as npm i -D @viz-js/viz @resvg/resvg-js for PNG.

Advanced Analysis

Simulating Dynamic Loads

If your app uses ship.load() to add modules at runtime, you can simulate this in the CLI using the --load flag: it takes path#export, compiles a module after the root, as ship.load() would, and repeats once per module:

npx nexusdi graph src/meridian.module.ts#Meridian --load src/science.module.ts#Science

Including Plugins

If you use plugins that add modules or change the graph, you can pass them via the --plugins flag: it takes path#export and names an exported array of plugins, the array you pass to Nexus.create:

npx nexusdi graph src/meridian.module.ts#Meridian --plugins src/plugins.ts#plugins

Drawing a Running Container’s Graph from JSON

A .json file written with JSON.stringify(graph(ship)) from a running app is an entry too. The command checks the file with parseGraph from @nexusdi/devtools, which reads a NexusGraph back from JSON.parse output and checks every field:

import { Nexus, Token, defineModule, provide } from '@nexusdi/core'; import { devtools, graph, parseGraph } from '@nexusdi/devtools'; interface IReactorCore { readonly output: number; } const REACTOR = new Token<IReactorCore>('ReactorCore'); class FusionReactor implements IReactorCore { readonly output = 1.21; } const Engineering = defineModule({ name: 'Engineering', providers: [provide(REACTOR, { useClass: FusionReactor })], }); await using ship = await Nexus.create(Engineering, { plugins: [devtools()] }); const saved = JSON.stringify(graph(ship)); const read = parseGraph(JSON.parse(saved)); const reactor = read.providers.find( (provider) => provider.token === 'ReactorCore', ); const built = reactor?.implementation; // -> 'FusionReactor' console.log(built);
npx nexusdi graph graph.json -f dot

A value that is not a NexusGraph throws DevtoolsError with the code NEXUS_DEVTOOLS_GRAPH_INVALID, and its path names the first bad field:

import { isNexusError } from '@nexusdi/core'; import { parseGraph } from '@nexusdi/devtools'; const edited = { modules: [], providers: [{ id: 'p0', token: 'ReactorCore' }], edges: [], }; let error: unknown; try { parseGraph(edited); } catch (caught) { error = caught; } const code = isNexusError(error) ? error.code : null; // -> 'NEXUS_DEVTOOLS_GRAPH_INVALID' console.log(code); const invalid = isNexusError(error, 'NEXUS_DEVTOOLS_GRAPH_INVALID') ? error : null; const path = invalid?.path; // -> 'providers[0].module' console.log(path); const lines = isNexusError(error) ? error.message.split('\n') : []; // -> ['[NEXUS_DEVTOOLS_GRAPH_INVALID] not a NexusGraph: providers[0].module is missing or has the wrong type.', ' Fix: pass JSON.parse of the text that JSON.stringify(graph(ship)), JSON.stringify(inspect(root)) or nexusdi graph --format json wrote.'] console.log(lines.join('\n'));

Try it: Delete the "module" field of one provider in a saved graph.json and run npx nexusdi graph graph.json. The command exits with code 2 and prints nexusdi: graph.json: [NEXUS_DEVTOOLS_GRAPH_INVALID] not a NexusGraph: providers[0].module is missing or has the wrong type. Put the field back and run it again: it prints the DOT text and exits with code 0.

CI/CD Integration

The nexusdi graph command is ideal for CI pipelines. It exits with code 0 if the graph is valid and code 1 if there are wiring mistakes. Code 2 means the arguments or an input file are wrong, and code 3 means a package the command needs is missing, such as @nexusdi/devtools or @viz-js/viz. A CI job runs the command and uploads the graph, and the job fails on any nonzero exit code. By adding this to your pipeline, you can prevent broken dependency graphs from ever being merged into your main branch.

# Fail the build if the graph is invalid npx nexusdi graph src/meridian.module.ts#Meridian -f json -o graph.json

The command exits with one of four codes. For codes 2 and 3, the message on stderr starts with nexusdi: and ends with the command or install line that fixes it.

CodeMeaning
0The graph was written.
1The graph is invalid. stderr holds the BlueprintError text.
2The command or its input is wrong, such as an unknown flag or a .json entry that is not a graph.
3Something is missing: @nexusdi/core, @nexusdi/devtools, tsx, @viz-js/viz or @resvg/resvg-js.