Heximon Logo
Under the Hood

Other bundlers

Compile a Heximon app and serve its generated wiring across bundlers with @heximon/build — the full dev experience on Vite (including WebSocket-in-dev), and Rollup / Rolldown / esbuild / webpack / rspack build tiers that emit a runnable server bundle from one unchanged src/.

Heximon's compile step is a build-time concern: walk the modules, build the DI graph, emit plain JS wiring into .heximon/. That work isn't tied to any one bundler — so @heximon/build exposes it as an unplugin factory with a per-bundler entry point. Your src/ never changes; you pick the bundler.

The @heximon/build/vite entry is the complete Vite plugin: the build-error page, the JSON-preview page, recompile-on-change, browser auto-reload, and dev WebSocket-upgrade handling — a ws:// route connects under pnpm dev.

The Rollup, Rolldown, esbuild, webpack, and rspack entries are build tiers: they run the compiler and let the bundler emit a runnable server bundle, with diagnostics to the terminal.

The live dev server is Vite-only — webpack and rspack have no way to run the compiled app in-process, so they compile and build but do not host the running app (use @heximon/build/vite, or the Nitro dev plugin, for that).

Which path should you use?

Most Heximon apps never hand-write a bundler config. Develop with @heximon/build/vite (pnpm dev — the full dev experience above) and deploy with Nitro, which owns the production build and targets Node, Workers, Deno, Bun, and Lambda from one codebase. Both are owned-build hosts: you add one plugin/module and never touch resolve, externals, or loaders — the same way SvelteKit or Nuxt hide their bundler.

Reach for the per-bundler build entries (/rollup, /rolldown, /esbuild, /webpack, /rspack) when you're folding Heximon into a project that already builds with one of those bundlers — a monorepo standardized on webpack, say.

On that path you write that bundler's ordinary config (entry, output, externals, and its standard .js.ts TypeScript handling — resolve.extensionAlias on webpack/rspack, a resolve plugin on esbuild/rollup, nothing on Rolldown), exactly as any TypeScript-ESM project on that bundler already does.

The gap's alternate-bundlers example shows a complete, runnable config per bundler.

Install

pnpm add -D @heximon/build

vite, rollup, rolldown, and esbuild are optional peer dependencies — install whichever bundler you actually use; the matching @heximon/build/<bundler> entry pulls in nothing else. webpack and rspack work the same way but are not declared as peers (their entries are typed structurally and import neither package) — install the bundler yourself.

Develop on Vite

@heximon/build/vite is a drop-in for the dev server. The plugin takes no arguments — Heximon config lives in heximon.config.ts, the same file createTestApp reads:

vite.config.ts
import heximon from "@heximon/build/vite";
import { defineConfig } from "vite-plus";

export default defineConfig({
  plugins: [heximon()],
  server: { port: 3002 },
});
heximon.config.ts
import { defineHeximonConfig } from "@heximon/build";
import { HttpPlugin } from "@heximon/http/compiler";

export default defineHeximonConfig({ plugins: [new HttpPlugin()] });

On pnpm dev you get the full surface: a source edit recompiles src/ and reloads the browser; a broken compile shows the diagnostic codeframe in the browser (and a real 500 to curl / a JSON client) instead of a stale response; a browser navigation to a JSON route renders the syntax-highlighted preview page; and a WebSocket route upgrades its ws:// connection automatically.

Build on Rollup, Rolldown, esbuild, webpack, or rspack

The build entries compile src/ to .heximon/ on buildStart (reporting compile diagnostics to the terminal); the bundler then bundles the generated server.js entry — and the src/ it imports — into your output. Framework packages (@heximon/*) are left external for the runtime to resolve. Each factory takes no arguments — compiler plugins are read from heximon.config.ts:

rolldown.config.ts
import heximon from "@heximon/build/rolldown";
import { fileURLToPath } from "node:url";
import { isAbsolute } from "node:path";
import { defineConfig } from "rolldown";

export default defineConfig({
  input: fileURLToPath(new URL(".heximon/server.js", import.meta.url)),
  plugins: [heximon()],
  resolve: {
    extensionAlias: { ".js": [".ts", ".js"] },
    conditionNames: ["import", "default"],
  },
  external: (id) => !id.startsWith(".") && !isAbsolute(id),
  output: { dir: "dist", format: "esm" },
});

The generated entry re-exports the web fetch handler and the disposable app, so the bundle is a runnable server (dist/server.js exporting { app, fetch }).

The Rollup, esbuild, webpack, and rspack configs follow the same shape via @heximon/build/rollup, @heximon/build/esbuild, @heximon/build/webpack, and @heximon/build/rspack.

Rolldown and esbuild transpile the app's .ts sources out of the box; Rollup, webpack, and rspack need a TypeScript loader of your choosing (e.g. webpack/rspack's builtin:swc-loader) so the bundler can read the src/ the generated wiring imports.

Package as a library

The same cross-bundler reach applies to library mode — packaging a Heximon module so another app can consume it.

@heximon/library is the bundler-agnostic library emit (separate from @heximon/build): it compiles in library mode, writes the typed create<Module>.ts factory + a src/.heximon/library/index.ts barrel, and emits dist/heximon.manifest.json.

It is exposed per bundler as a default-export factory — @heximon/library/vite, @heximon/library/rollup, @heximon/library/rolldown, @heximon/library/esbuild.

Under vp pack (Rolldown/tsdown) the factory ships on the package's dedicated ./heximon subpath, fully wired by the plugin. On the other bundlers you add the generated barrel (src/.heximon/library/index.ts) as a second entry and the ./heximon export-map entry yourself — the emit detects the committed entry and shapes the manifest to match:

// build.mjs (packaging a module with esbuild)
import heximonLibrary from "@heximon/library/esbuild";
import { build } from "esbuild";

await build({
  entryPoints: ["src/index.ts"],
  bundle: true,
  format: "esm",
  outdir: "dist",
  external: ["@heximon/*"], // the framework stays a single cross-boundary copy
  plugins: [heximonLibrary()], // compiler plugins are read from heximon.config.ts
});

Whichever bundler you package with, the published factory and manifest are identical. See the library mode guide for the boundary module, the peerDependencies rule, and how a host consumes the package.

Capability matrix

The dev experience needs a dev server, which only Vite exposes through unplugin. Every entry compiles and serves the generated wiring; the tiers differ in what the bundler offers on top.

EntryCompile + wiringRecompile on changeDev server (error page · JSON preview · auto-reload · ws:// upgrade)Tier
@heximon/build/vite✅ in-memory✅ full (incl. WebSocket-in-dev)Full dev
@heximon/build/rollup✅ to disk✅ (watch)❌ terminal diagnosticsBuild
@heximon/build/rolldown✅ to disk✅ (watch)❌ terminal diagnosticsBuild
@heximon/build/esbuild✅ to disk❌ (no watch hook)❌ terminal diagnosticsBuild
@heximon/build/webpack✅ to disk✅ (per rebuild)❌ terminal diagnosticsBuild
@heximon/build/rspack✅ to disk✅ (per rebuild)❌ terminal diagnosticsBuild

Only Vite exposes a dev server through unplugin, and only Vite can run the compiled app in-process — so webpack and rspack are build targets only (compile + diagnostics + a runnable bundle), with no live dev server. Use rspack ≥ 1.5.0 (older versions write placeholder virtual-module files that collide with the .heximon/ write).

How it shares the dev experience

@heximon/build is a thin adapter over the same engine the Vite and Nitro hosts use. The build-time-only @heximon/dev package owns the compile / serve-last-good / recompile core (DevServerCore) behind a small DevHost adapter, plus the shared Vite host kit (ViteDevClient, ViteDevHost, attachViteDevServer) at its ./vite subpath.

The @heximon/build/vite entry binds that kit to Vite's dev server, so the build-error page, JSON preview, and auto-reload are the same code the Nitro host runs — not a reimplementation. The /vite entry adds the WebSocket upgrade handler on top.

See also

  • Hosts overview — every host that serves the compiled app, and how to pick one.
  • Installation — set up the @heximon/build/vite dev server this page builds on.
  • Packages@heximon/build and @heximon/dev in the package stack.
  • Cross-bundler build — the same app served by pnpm dev (Vite) and built to a runnable bundle by all five build entries (build:rolldown / build:esbuild / build:rollup / build:webpack / build:rspack, or build:all).
Copyright © 2026