Publish a library for bun compile

A package points bun compile at its TypeScript with the aot condition of its exports. Everyone else sees an ordinary package

Ahead-of-time compilation is experimental.

bun compile generates better code when it knows the types. Most packages on npm ship JavaScript and .d.ts files, so the types inside their functions are gone. A package gives them back with one line in its package.json: the "aot" condition.

package.json
{
  "name": "greeter",
  "version": "1.0.0",
  "type": "module",
  "exports": {
    ".": {
      "aot": "./dist/index.aot.ts",
      "types": "./dist/index.d.ts",
      "default": "./dist/index.js"
    }
  }
}

How it works#

"aot" is an ordinary condition of exports, like "node" or "browser".

Commandimport "greeter" loads
bun compile, bun build --compile --aot, bun run --aot, bun test --aotdist/index.aot.ts
bun check --aotdist/index.aot.ts
bun run, bun test, bun build, Node.js, tsc, editors and other bundlersdist/index.js
  • The file is the module. Bun compiles it like any TypeScript file of a dependency. Nothing compares it with index.js, so keeping the two in step is up to you. Generating both from the same sources does that.
  • A wrong type never changes what a program does. A type is a prediction that a guard checks. A value of another type takes a slower path.
  • The type checker reads the same file. Under --aot, "types" isn't read for an export that has an "aot" condition, whatever the order of the keys, so every class has one declaration. What the file exports is what importers see: a class written as export var Box = (() => { class Box {} return Box; })() is a value there and can't be used as a type.
  • Put "aot" first. As with every condition, the first one that matches wins: after "default", or after an "import" that names a file, "aot" is never reached.
  • It works wherever conditions work: nested under "import" or "require", on subpaths, on patterns such as "./features/*", and in "imports".
  • Relative imports are resolved as always. Only imports that go through exports or imports see the condition, so a typed file imports the typed files beside it by name: import { parse } from "./parse.aot.ts". As in TypeScript, "./parse.js" also finds parse.ts, "./parse.mjs" finds parse.mts, and "./parse.cjs" finds parse.cts.
  • The name of the file is up to you. It can be your source: "aot": "./src/index.ts", if you publish src.

Compiler options#

Bun type checks and compiles the TypeScript of a package with an "aot" condition under the "compilerOptions" of that package's package.json, or as strict if it has none. The tsconfig.json and the flags of the program that installs the package don't apply, so the package behaves the same in every program.

package.json
{
  "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true }
}

State the options that change what your code does, if your build sets them: useDefineForClassFields, experimentalDecorators, emitDecoratorMetadata, jsx, jsxFactory, jsxFragmentFactory and jsxImportSource. As everywhere in Bun, target and verbatimModuleSyntax don't change the code.

In the package you're working on, the one you run Bun in, your tsconfig.json applies as always.

Type errors#

If the files have type errors in the program that installs the package, the build continues and compiles them without their types:

warn: TS100009: 'greeter' has 1 type error (the first at node_modules/greeter/dist/index.aot.ts:3:7), so 1 of its 1 files are compiled without their types. They still work, but run slower.

Syntax errors and missing files#

A file that doesn't parse stops the build, and so does an "aot" condition that names a file the package doesn't have. The file is the module, so Bun doesn't fall back to index.js, just as Node.js doesn't try the next condition when a target is missing. With a pattern such as "./features/*", every subpath that a program imports needs its own file.

Let Bun write the files#

bun compile --library builds a package that's ready for npm, "aot" condition included.

src/index.ts
export interface Options {
  name: string;
  times: number;
}

export function greet(options: Options): string {
  return ("hello " + options.name + " ").repeat(options.times).trim();
}
package.json
{
  "name": "greeter",
  "version": "1.0.0",
  "license": "MIT",
  "scripts": { "test": "bun test" },
  "devDependencies": { "@types/bun": "latest" }
}
terminal
bun compile --library
  index.js      135 bytes  (entry point)
  index.d.ts    123 bytes  (declaration)
  index.aot.ts  232 bytes  (aot source)
  package.json  450 bytes  (asset)
FileHoldsRead by
index.jsThe bundle, as ES modulesEvery runtime and bundler
index.d.tsTypes for everything index.js exportsEditors, tsc, bun check
index.aot.tsindex.js with the types of your sources put backbun compile
package.jsonYour package.json, rewritten for the files abovenpm and whoever installs it

Bun type checks the package first, and a type error fails the build. It keeps the fields of your package.json that describe the package, such as name, version, license and dependencies. It leaves out scripts, devDependencies, private and workspaces, and writes its own type, main, module, types, exports, files and compilerOptions. It also copies your README and LICENSE files. Publish the folder with bun publish from dist.

Keep your own package.json#

bun build --dts writes the same .js, .d.ts and .aot.ts files to --outdir and leaves your package.json alone. Add the condition yourself, as at the top of this page.

terminal
bun build ./src/index.ts --outdir dist --target node --dts

What has no .aot.ts file yet#

With --minify-identifiers (which --minify includes), --no-bundle, --format=cjs or --sourcemap=inline, Bun writes the JavaScript and the .d.ts files and warns that it left out the rest:

warn: the .aot.ts files that --aot compiles a package from are left out: they do not support --minify-identifiers yet

Bun also type checks each .aot.ts file before it writes it, the way a program that installs your package does. If a file has a type error, the build warns you and leaves the files out.

Test before you publish#

terminal
cd dist && bun pm pack            # greeter-1.0.0.tgz
cd ../../app && bun add ../greeter/dist/greeter-1.0.0.tgz
bun check --aot ./main.ts         # type checks what bun compile compiles, your package included
bun compile ./main.ts --explain   # shows where types are missing

bun run --aot ./main.ts and bun test --aot run your .aot.ts files without compiling them, so your package's own tests can cover them.