Skip to content

Bun

Nola runs on Bun two ways. Built output needs nothing: nola build writes plain JavaScript that bun runs as it is. For .tsi from source — bun src/main.tsi, bun --watch, bun test, one-step builds — add the Bun plugin. Bun ≥ 1.3 is required.

Terminal window
bun add -d @nola-lang/bun
{
"scripts": {
"start": "bun src/main.tsi",
"build": "nola build",
"check": "nola check"
},
"dependencies": {
"@nola-lang/providers": "^0.1.15",
"@nola-lang/runtime": "^0.1.15"
},
"devDependencies": {
"@nola-lang/bun": "^0.1.15",
"nola-lang": "^0.1.15",
"typescript": "^5.6.0"
},
"engines": { "node": ">=22.18", "bun": ">=1.3.0" }
}

bun create nola scaffolds this layout; --runtime node keeps the Node layout instead, and --runtime bun asks for it under any package manager.

bunfig.toml
preload = ["@nola-lang/bun/preload"]
[test]
preload = ["@nola-lang/bun/preload"]

The preload registers the plugin and applies the nearest nola.config.ts — Bun imports it directly and loads .env itself, so no loader and no bundling step is involved.

Terminal window
bun src/main.tsi
bun --watch src/main.tsi

Every .tsi the program imports lowers on load; ./models.tsi imports of plain TypeScript modules resolve to their views as under the Node loader. bun --watch restarts the process on a change and is the supported watch mode (bun --hot keeps the process: a changed .tsi re-lowers, but a change to a plain .ts a .tsi derives types from can serve a stale derivation until a restart).

With the [test] preload above, test files import .tsi modules:

import { expect, test } from "bun:test";
import { describePerson } from "./src/describe.tsi";
test("describes", async () => {
expect(await describePerson("Ada")).toBe("a poet");
});

Bun’s test runner discovers its own file extensions, so a .tsi cannot itself be a test file.

Bun.build takes the plugin; the bun build command line does not take plugins, so use a build script:

// not-checked — build.ts (the `Bun` global needs bun-types)
import { nola } from "@nola-lang/bun";
await Bun.build({
entrypoints: ["src/main.tsi"],
plugins: [nola()],
target: "bun",
outdir: "dist",
// or a single executable instead of outdir:
// compile: { outfile: "my-app" },
});
Terminal window
bun build.ts

For application builds the plugin wires nola.config.ts into the bundle, as nola build and the bundler plugins do; build: { target: "lib" } in the config, or nola({ config: false }), leaves configuration to the consuming application. Options: target ("app" | "lib") and config (a path override, or false).

A Bun.build for target: "browser" fails with NOLA4001: Nola execution is server-side only.

nola build output runs under Bun unchanged, so a Node build step and a Bun runtime also work:

Terminal window
npx nola-lang build --out dist
bun dist/src/main.tsi.js
bun build --compile dist/src/main.tsi.js --outfile my-app

See Deploying for what the build emits.

The nola command line — nola check, nola build, nola declarations, nola console — runs on Node; bun run check launches it there by the bin’s shebang. nola run is the Node loader: started under Bun it fails with NOLA3015, which names the preload above. Debugging .tsi in VS Code (F5) runs under the Node loader too.

  • Bun plugins carry no source map, so a stack trace through .tsi code shows lowered positions: at module level and inside plain functions the line is right; inside an infer function body the reported line is one too high (the lowered wrapper opens on a line of its own above the body); a column inside an ask may drift.
  • A Bun older than 1.3 is refused with NOLA3023.
  • nola.config.ts cannot import .tsi modules (NOLA3012); a .tsi import that names neither a Nola file nor a TypeScript module is NOLA2007.
  • Deno has no module hooks and no plugin API; run on Node there.

Next: VS Code