Publish a Feature Package
You want to ship a whole feature — its providers, its controllers, its boundary — as an npm package, so
another app pulls it in with a single import { BillingModule } from "@acme/billing". The package ships
precompiled: heximonLibrary() runs the compiler at build time and emits a createBillingModule factory
plus a heximon.manifest.json describing the boundary.
The consuming app never re-reads your source — both sides align on DI identity through the shared module class and the manifest's recorded import specifiers.
You'll end up with:
- A package whose internal DI graph is compiled into a
createBillingModulefactory - A boundary that requires the host's database token and exports its service
- A consuming app that imports the module by its bare specifier and routes the required token
How a published boundary works
A published module declares three cross-boundary token sets. The package owns its requires token as a
concrete class with a default the host overrides — the package never constructs it across the boundary; the
host supplies the instance.
| Field | Meaning at the boundary |
|---|---|
requires | tokens the host must supply (the app routes them) |
providers | the package's internal classes the factory constructs |
exports | tokens the consuming app may inject |
Write the package
// The package's OWN database token. The package `requires` and `exports` it; the host binds it to a
// concrete database. Declared concrete (a class is the DI token) with a default the host overrides.
export class BillingDatabase {
public async read(_id: string): Promise<string | undefined> {
return undefined;
}
}
import { BillingDatabase } from "./billing.database";
export class BillingService {
// The host-bound database, supplied by the factory across the boundary.
public constructor(private readonly billingDatabase: BillingDatabase) {}
public async invoiceAmount(id: string): Promise<string | undefined> {
return this.billingDatabase.read(id);
}
}
import { Module } from "@heximon/runtime";
import { BillingDatabase } from "./billing.database";
import { BillingService } from "./billing.service";
export class BillingModule extends Module({
requires: [BillingDatabase], // the host supplies this token
providers: [BillingService], // constructed by the generated factory (injects the database)
exports: [BillingService], // the consuming app may inject this
}) {}
A package shipping an HTTP controller declares it under http: { controllers } exactly as an app does;
the library build with new HttpPlugin() ships the controller's route registration as a contribution the
host adds at boot:
import { Module } from "@heximon/runtime";
import { CatalogDatabase } from "./catalog.database";
import { ShopController } from "./shop.controller";
import { ShopRepository } from "./shop.repository";
export class ShopModule extends Module({
requires: [CatalogDatabase],
providers: [ShopRepository],
http: { controllers: [ShopController] }, // shipped as a route contribution
}) {}
Expose the public surface
Export every cross-boundary token from one bare specifier. The manifest records each token's specifier
as the package name, so both sides align on the same DI identity. The co-generated create<Module>
factory is not part of this barrel — it ships on the package's ./heximon subpath, wired by the build.
export { BillingDatabase } from "./billing.database";
export { BillingModule } from "./billing.module";
export { BillingService } from "./billing.service";
Build it with heximonLibrary()
Add the heximonLibrary() plugin (@heximon/library/rolldown) to your vp pack pack.plugins. On vp pack it
generates the typed createBillingModule factory + a src/.heximon/library/index.ts barrel, registers that barrel
as the pack's own heximon entry (the ./heximon subpath, written into the package.json exports map for
you), and emits dist/heximon.manifest.json describing the boundary.
Compiler plugins come from heximon.config.ts:
import heximonLibrary from "@heximon/library/rolldown";
import { defineConfig } from "vite-plus";
export default defineConfig({
pack: {
entry: ["src/index.ts"],
dts: true,
format: ["esm"],
plugins: [heximonLibrary()],
},
});
{
"name": "@acme/billing",
"version": "1.2.3",
"type": "module",
"files": ["dist"],
"exports": {
".": { "default": "./dist/index.mjs" }
},
"peerDependencies": {
"@heximon/runtime": "workspace:*"
}
}
peerDependencies, never bundled. Two copies
of @heximon/runtime across the boundary split DI token identity at runtime, and a BillingService
constructed by the package would no longer match the BillingService the host injects.heximonLibrary() warns when no @heximon/* peer is declared, or when files doesn't cover dist/. One
copy, shared as a peer, keeps the tokens identical.Build the package:
pnpm --filter @acme/billing build
npm run build --workspace @acme/billing
bun run --filter @acme/billing build
Consume it from an app
Import the module by its bare specifier and list it in imports. The app's compiler reads the package's
heximon.manifest.json to wire the boundary — it never re-reads the package source.
import { Module } from "@heximon/runtime";
import { BillingModule } from "@acme/billing";
export class AppModule extends Module({
imports: [BillingModule],
}) {}
Now route the package's required token to a concrete provider in the host. The host binds BillingDatabase
to its own database with useExisting:
import { Module } from "@heximon/runtime";
import { BillingDatabase } from "@acme/billing";
import { PrimaryDatabase } from "./primary-database";
export class DatabaseModule extends Module({
providers: [
PrimaryDatabase,
{ provide: BillingDatabase, useExisting: PrimaryDatabase }, // route the package's required token
],
exports: [BillingDatabase, PrimaryDatabase],
}) {}
Any app class can now inject the package's exported BillingService by class identity:
import { BillingService } from "@acme/billing";
import type { Controller, Get } from "@heximon/http";
export class InvoicesController implements Controller<"/invoices"> {
public constructor(private readonly billing: BillingService) {}
public async amount(action: Get<"/:id/amount">): Promise<{ id: string; amount: string | undefined }> {
const id = action.request.pathParams.id;
return { id, amount: await this.billing.invoiceAmount(id) };
}
}
Run it
Build the package's dist — factory and manifest — before the consuming app resolves it, then start the
app:
pnpm --filter @acme/billing build
pnpm dev
curl -s localhost:3000/invoices/inv_42/amount
# -> { "id": "inv_42", "amount": "..." } (read through the HOST's database, bound to the package token)
npm run build --workspace=@acme/billing
npm run dev
curl -s localhost:3000/invoices/inv_42/amount
# -> { "id": "inv_42", "amount": "..." } (read through the HOST's database, bound to the package token)
yarn workspace @acme/billing build
yarn run dev
curl -s localhost:3000/invoices/inv_42/amount
# -> { "id": "inv_42", "amount": "..." } (read through the HOST's database, bound to the package token)
bun run --filter @acme/billing build
bun run dev
curl -s localhost:3000/invoices/inv_42/amount
# -> { "id": "inv_42", "amount": "..." } (read through the HOST's database, bound to the package token)
See also
- Library Mode —
heximonLibrary(), thecreate<Module>factory, the manifest, and therequires/exportsboundary in depth. - Modules — how modules declare
requires,providers, andexports, and howimportscomposes them across boundaries. - Build a CRUD API — build a complete typed feature end-to-end before shipping it as a published package.
Build a DDD Aggregate
Model an Order aggregate root with branded ids, value objects, invariants, DomainEvent recording, DomainEvents.emitFrom flushing, and a cross-context DomainEventHandler.
Reliable Integration Events
Transactional outbox pattern for at-least-once integration event delivery — OutboxStore, OutboxIntegrationEventTransport, OutboxRelay, fire-and-forget tradeoff, the fire-and-forget compiler warning and its suppressDiagnostics opt-out.