Skip to main content

NexusDI 0.3.2

· 5 min read
Mikael Pettersson
NexusDI Author

@nexusdi/core 0.3.2 is on npm under the latest dist-tag, published with npm provenance. It fixes three faults that 0.3.0 and 0.3.1 share, and it removes an unused dependency. It also declares which of its modules have side effects, so a bundler keeps the Symbol.metadata polyfill. 0.3.2 changes no exported name or signature, so an upgrade needs no code change.

Upgrade​

npm i @nexusdi/core@0.3.2

0.3.2 declares "engines": { "node": ">=20" }.

Node ESM imports​

In 0.3.0 and 0.3.1, the files in dist/ imported each other without file extensions, for example from './constants'. Node's ESM resolver requires the extension, so import { Nexus } from '@nexusdi/core' failed with ERR_MODULE_NOT_FOUND in any program Node ran without a bundler. Bundlers resolve extensionless paths, so bundled applications did not fail. 0.3.2 writes every relative import with its .js extension, and an import of a directory names its index.js.

The development export condition​

The 0.3.0 and 0.3.1 export maps had a development condition that pointed at ./src/index.ts. Both packages contained only dist/, so that file did not exist in node_modules. Tools that enable the development condition, such as the Vite dev server and Vitest, resolved @nexusdi/core to the missing file and failed. 0.3.2 removes the condition. The import and default conditions resolve to dist/index.js, and types resolves to dist/index.d.ts.

The Symbol.metadata polyfill under tree-shaking​

dist/index.js installs a Symbol.metadata polyfill when the runtime has none. The polyfill is a module-level statement with no export, and a bundler removes such a statement when the package declares its modules free of side effects. 0.3.2 adds a sideEffects field to package.json that lists dist/index.js. A bundler keeps the polyfill, and it can drop the other modules that an application never imports.

Token after a bundler renames the class​

0.3.0 and 0.3.1 recognised a Token by checking that the name of its constructor was Token. esbuild renames a top-level class when two modules in one bundle declare the same name, so the bundle can contain the class as _Token. The container then rejected a valid token with InvalidToken. Applications failed this way whenever their bundler renamed the class.

0.3.2 checks instanceof Token first. The Token constructor also marks every instance under the registered symbol Symbol.for('nexusdi.token'), and the check accepts an object that carries the mark. A token created by a second copy of @nexusdi/core in the same bundle passes the check the same way.

tslib​

0.3.0 and 0.3.1 listed tslib in dependencies, and no file in dist/ imported it. 0.3.2 removes it, and @nexusdi/core has no runtime dependencies.

Decorators in 0.3​

The post that announced 0.3 and several pages on this site described the 0.3 decorators incorrectly. NexusDI 0.3 uses TypeScript's legacy decorators, and it requires experimentalDecorators in tsconfig.json:

{
"compilerOptions": {
"experimentalDecorators": true
}
}

0.3 does not support standard (TC39) decorators. The 0.3 decorators store their metadata under the Symbol.metadata key, and the package polyfills Symbol.metadata when the runtime lacks it. NexusDI 0.3 reads no metadata that emitDecoratorMetadata emits, so a project does not need that option. The 0.3.2 README and the pages on this site now state the requirement.

NexusDI 0.4​

We want dependency injection that fits the toolchain you already use, with no special compiler settings, and that tells you what is wrong with your wiring before your app starts. That is why we are working on NexusDI 0.4. Here are the parts we are most excited about.

0.4 checks your whole graph before it builds anything. Nexus.create validates every module and provider before any constructor runs, and it reports every wiring mistake at once: missing providers, singletons that depend on scoped providers, and dependency cycles with their full path, such as ShieldGrid → PowerRouter → ShieldGrid.

Startup is async, and get() stays synchronous. You await Nexus.create once. It builds your singletons, awaits async factories and runs onInit(). After that, every get() is synchronous, so you never need to know which providers were async.

Decorators become optional. You list a provider's dependencies in a typed deps tuple with provide(), and TypeScript checks the tuple against the constructor. If you like decorators, @Injectable, @Inject and @Module remain as standard TC39 decorators, and experimentalDecorators goes away.

The container has zero dependencies. With provide() and defineModule(), it runs on standard JavaScript, with no compiler flags and no build plugins.

Here is a preview of the 0.4 shape. It may change before release:

import { Nexus, Token, defineModule, provide } from '@nexusdi/core';
import { createTestingContainer } from '@nexusdi/core/testing';

interface IReactorCore {
output(): number;
}

const REACTOR = new Token<IReactorCore>('ReactorCore');

class FusionReactor implements IReactorCore {
output() {
return 1.21;
}
}

class ShipComputer {
constructor(private reactor: IReactorCore) {}
}

const Engineering = defineModule({
name: 'Engineering',
providers: [
provide(REACTOR, { useClass: FusionReactor }),
provide(ShipComputer, { deps: [REACTOR] }),
],
exports: [ShipComputer],
});

await using ship = await Nexus.create(Engineering);
const computer = ship.get(ShipComputer); // synchronous

// In a test, swap the implementation behind the interface:
class FakeReactor implements IReactorCore {
output() {
return 0;
}
}

await using testShip = await createTestingContainer(Engineering)
.override(REACTOR, { useClass: FakeReactor })
.create();

Most of the upgrade to 0.4 will be one command. npx @nexusdi/codemod 0.4 rewrites a 0.3 project and reports each change it could not make, and a migration guide maps every 0.3 API to its 0.4 form.

Release candidates come first, on the npm next dist-tag, and latest stays on 0.3.x until 0.4.0 final. Follow along and tell us what you think in GitHub Discussions.