Heximon Logo
Recipes

Publish a Feature Package

Ship a feature as a precompiled, publishable Heximon module — build the factory and manifest with heximonLibrary(), require host tokens, then consume it in one import.

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 createBillingModule factory
  • 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.

FieldMeaning at the boundary
requirestokens the host must supply (the app routes them)
providersthe package's internal classes the factory constructs
exportstokens the consuming app may inject

Write the package

src/billing.database.ts
// 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;
  }
}
src/billing.service.ts
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);
  }
}
src/billing.module.ts
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:

src/shop.module.ts (HTTP feature variant)
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.

src/index.ts
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:

vite.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()],
  },
});
package.json
{
  "name": "@acme/billing",
  "version": "1.2.3",
  "type": "module",
  "files": ["dist"],
  "exports": {
    ".": { "default": "./dist/index.mjs" }
  },
  "peerDependencies": {
    "@heximon/runtime": "workspace:*"
  }
}
The framework and any shared cross-boundary types must be 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

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.

src/app.module.ts
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:

src/database/database.module.ts
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:

src/invoices/invoices.controller.ts
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)

See also

  • Library ModeheximonLibrary(), the create<Module> factory, the manifest, and the requires/exports boundary in depth.
  • Modules — how modules declare requires, providers, and exports, and how imports composes them across boundaries.
  • Build a CRUD API — build a complete typed feature end-to-end before shipping it as a published package.
Copyright © 2026