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.
Introspection and Tracing
When working with a large dependency graph, it can be difficult to visualize how providers are connected or to understand exactly when an instance is being created.
The @nexusdi/devtools plugin provides “introspection” tools that allow you to inspect the compiled graph as data or trace the container’s execution as a stream of events. graph(ship) and inspect(root) return the module graph as plain JSON, toMermaid() and toDot() draw it as text, and trace(fn) hands every lifecycle event to a callback.
Enabling DevTools
DevTools is a plugin that should be registered in your development environment. In production, it is typically omitted to keep the bundle size minimal and prevent leaking internal architectural details. devtools() enables graph(ship) for a container and formats its errors with @nexusdi/errors. Install it with npm install -D @nexusdi/devtools@next.
import { Nexus, Token, defineModule, provide } from '@nexusdi/core';
import { devtools } from '@nexusdi/devtools';
interface IReactorCore {
readonly output: number;
}
const REACTOR = new Token<IReactorCore>('ReactorCore');
class FusionReactor implements IReactorCore {
readonly output = 1.21;
}
const Meridian = defineModule({
name: 'Meridian',
providers: [provide(REACTOR, { useClass: FusionReactor })],
});
const dev = process.env.NODE_ENV !== 'production';
await using ship = await Nexus.create(Meridian, {
plugins: dev ? [devtools()] : [],
});
const output = ship.get(REACTOR).output; // -> 1.21
console.log(output);Try it: Save the region as dev.ts and run npx tsx dev.ts. It prints 1.21. Run it again with NODE_ENV=production npx tsx dev.ts: it prints 1.21 again, because the container works the same with no plugin registered. Calling graph(ship) in that second run would throw, which the next section shows.
Visualizing the Graph with graph()
The graph(ship) function returns the entire compiled dependency graph as a structured JSON object. This graph contains three key arrays:
- Modules: The names and visibility (global/private) of every module. A module has a
name,global,importsandexports. - Providers: Details on every provider, including its token, lifetime (singleton/transient), and whether it is eager or async. A provider has a
token(the token’s description or the class name),module,lifetime,kind(class,value,factoryoralias),eager,async, the annotators’notes, theimplementationclass name andinternal. - Edges: Every dependency link between providers, labeled by type (required, optional, lazy). The
kindof an edge isrequired,optional,lazy,alloralias.
import { Nexus, Token, defineModule, provide } from '@nexusdi/core';
import { devtools, graph } from '@nexusdi/devtools';
interface IReactorCore {
readonly output: number;
}
interface IShipComputer {
readonly reactor: IReactorCore;
}
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;
constructor(readonly reactor: IReactorCore) {}
}
const Engineering = defineModule({
name: 'Engineering',
providers: [
provide(REACTOR, { useClass: FusionReactor }),
provide(COMPUTER, { useClass: QuantumComputer }),
],
exports: [COMPUTER],
});
const Meridian = defineModule({ name: 'Meridian', imports: [Engineering] });
await using ship = await Nexus.create(Meridian, { plugins: [devtools()] });
const map = graph(ship);
const modules = map.modules.map((module) => module.name); // -> ['Meridian', 'Engineering']
console.log(modules);
const computer = map.providers.find((p) => p.token === 'ShipComputer');
const lifetime = computer?.lifetime; // -> 'singleton'
const kind = computer?.kind; // -> 'class'
const implementation = computer?.implementation; // -> 'QuantumComputer'
console.log(computer);
const edges = map.edges.map((edge) => edge.kind); // -> ['required']
console.log(edges);graph() throws NEXUS_DEVTOOLS_UNREGISTERED for a container created without devtools(). Ids such as p0 are stable for one set of definitions; compare them as whole strings. A provider with internal: true, such as core’s REQUEST, is an internal provider that every graph lists.
Try it: Save the region as graph.ts and run it. It prints the module names [ 'Meridian', 'Engineering' ], the ShipComputer provider with lifetime: 'singleton', kind: 'class' and implementation: 'QuantumComputer', and [ 'required' ] for the one edge. Change { plugins: [devtools()] } to { plugins: [] } and run it again: graph(ship) throws DevtoolsError: [NEXUS_DEVTOOLS_UNREGISTERED], and its message ends with the fix, Nexus.create(Root, { plugins: [devtools()] }). Put devtools() back and the first output returns.
Checking a Graph Without Building It
inspect(root) compiles a graph with Nexus.check and builds nothing, so no constructor runs. It takes the root forms Nexus.create takes and returns the same JSON as graph(), with every provider’s async set to null, since nothing ran. It throws the BlueprintError that Nexus.check throws.
You can also use toMermaid() or toDot() to convert this JSON graph into text that can be rendered as a visual diagram using Mermaid.js or Graphviz. Both take the graph, as in toMermaid(graph). Neither imports a Node module, so both run in a browser too.
import { Token, defineModule, provide } from '@nexusdi/core';
import { inspect, toDot, toMermaid } from '@nexusdi/devtools';
interface IReactorCore {
readonly output: number;
}
interface IShipComputer {
readonly reactor: IReactorCore;
}
const REACTOR = new Token<IReactorCore>('ReactorCore');
const COMPUTER = new Token<IShipComputer>('ShipComputer');
let built = 0;
class FusionReactor implements IReactorCore {
readonly output = 1.21;
constructor() {
built++;
}
}
class QuantumComputer implements IShipComputer {
static deps = [REACTOR] as const;
constructor(readonly reactor: IReactorCore) {
built++;
}
}
const Engineering = defineModule({
name: 'Engineering',
providers: [
provide(REACTOR, { useClass: FusionReactor }),
provide(COMPUTER, { useClass: QuantumComputer }),
],
exports: [COMPUTER],
});
const Meridian = defineModule({ name: 'Meridian', imports: [Engineering] });
const map = inspect(Meridian);
const constructed = built; // -> 0
console.log(constructed);
const mermaid = toMermaid(map);
const mermaidStart = mermaid.split('\n')[0]; // -> 'flowchart LR'
console.log(mermaid);
const dot = toDot(map);
const dotStart = dot.split('\n')[0]; // -> 'digraph nexus {'
console.log(dot);The nexusdi graph command draws the same text, plus SVG and PNG, from a shell: Visualizing Your Dependency Graph.
Try it: Save the region as draw.ts and run it. It prints 0 for the constructors that ran, then the Mermaid text, which starts with flowchart LR, and the DOT text, which starts with digraph nexus {. Pass { view: 'modules' } as the second argument of toMermaid and run it again: the Mermaid text becomes Meridian<br/>0 providers and Engineering<br/>2 providers joined by one arrow, the module import. Remove the argument and the provider drawing returns.
Real-time Tracing with trace()
While the graph shows the structure, tracing shows the behavior. By passing a callback to the trace() plugin from @nexusdi/devtools (or to the trace option of devtools()), you can observe every lifecycle event as it happens.
import { Nexus, Token, provide } from '@nexusdi/core';
import { trace } from '@nexusdi/devtools';
import type { TraceEvent } from '@nexusdi/devtools';
interface IReactorCore {
readonly output: number;
}
interface IShipComputer {
readonly reactor: IReactorCore;
}
const REACTOR = new Token<IReactorCore>('ReactorCore');
const COMPUTER = new Token<IShipComputer>('ShipComputer');
class FusionReactor implements IReactorCore {
readonly output = 1.21;
[Symbol.dispose]() {}
}
class QuantumComputer implements IShipComputer {
static deps = [REACTOR] as const;
constructor(readonly reactor: IReactorCore) {}
onInit() {}
}
const events: TraceEvent[] = [];
const ship = await Nexus.create(
[
provide(REACTOR, { useClass: FusionReactor }),
provide(COMPUTER, { useClass: QuantumComputer }),
],
{ plugins: [trace((event) => events.push(event))] },
);
{
await using shuttle = await ship.createScope();
}
await ship[Symbol.asyncDispose]();
const types = events.map((event) => event.type); // -> ['compile', 'construct', 'construct', 'init', 'scope:create', 'scope:dispose', 'dispose:instance', 'dispose']
console.log(types);Common events include:
construct: When a new instance is built.init: When a singleton’sonInitmethod completes.scope:create/scope:dispose: When scopes are created or disposed.dispose:instance: When a specific instance is cleaned up.
Four more types exist:
type | When NexusDI emits it |
|---|---|
compile | once per compile, at create, load or Nexus.check |
untracked | for a disposable instance that no container or scope owns |
scope:extend | when scope.extend() built what a later load() added |
dispose | when the container finished disposing |
A construct event carries its token, lifetime and scope. scope:create fires when createScope() finished building its scoped factories, and dispose:instance fires for each instance in disposal order.
The computer has an onInit, so one init event follows the two construct events. Only the reactor has a disposer, so one dispose:instance event comes before dispose.
Try it: Save the region as trace.ts and run it. It prints the eight events’ types in the order compile, construct, construct, init, scope:create, scope:dispose, dispose:instance, dispose. Add [Symbol.dispose]() {} to QuantumComputer and run it again: a second dispose:instance appears before dispose, and the computer’s comes first, because NexusDI disposes in reverse order of construction. Delete that line and the list has one dispose:instance again.
Handling Minification
In production, minifiers often rename classes to single letters (e.g., FusionReactor becomes t). This can make error messages and graphs unreadable. NexusDI names a provider in errors, in graph() and in trace events by its token: the description of a Token<T>, or the name of a class used as its own token.
To prevent this, you should always provide a human-readable description when creating a Token<T>. NexusDI uses this description for all introspection and error reporting, ensuring that your logs remain meaningful regardless of minification. To keep the class names too, configure the bundler to preserve them (keepNames in esbuild).
import { Token, defineModule, provide } from '@nexusdi/core';
import { inspect } from '@nexusdi/devtools';
interface IReactorCore {
readonly output: number;
}
const REACTOR = new Token<IReactorCore>('ReactorCore');
// A minifier gives a class a short name such as `t`.
const t = class implements IReactorCore {
readonly output = 1.21;
};
const map = inspect(
defineModule({
name: 'Engineering',
providers: [provide(REACTOR, { useClass: t }), t],
}),
);
const shown = map.providers.filter((p) => !p.internal);
const names = shown.map((p) => p.token); // -> ['ReactorCore', 't']
console.log(names);The provider bound to REACTOR keeps the name ReactorCore whatever the class is called. The region also registers the class t as its own token, and that provider shows the minified name.
Try it: Save the region as names.ts and run it. It prints [ 'ReactorCore', 't' ]. Rename the class constant t to FusionReactor in its declaration and in both places the providers list uses it, and run it again: it prints [ 'ReactorCore', 'FusionReactor' ], so the second name follows the class and the first stays ReactorCore. Rename the constant back to t and the second name is t again.