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
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.
{
"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".
| Command | import "greeter" loads |
|---|---|
bun compile, bun build --compile --aot, bun run --aot, bun test --aot | dist/index.aot.ts |
bun check --aot | dist/index.aot.ts |
bun run, bun test, bun build, Node.js, tsc, editors and other bundlers | dist/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 asexport 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
exportsorimportssee 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 findsparse.ts,"./parse.mjs"findsparse.mts, and"./parse.cjs"findsparse.cts. - The name of the file is up to you. It can be your source:
"aot": "./src/index.ts", if you publishsrc.
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.
{
"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.
export interface Options {
name: string;
times: number;
}
export function greet(options: Options): string {
return ("hello " + options.name + " ").repeat(options.times).trim();
}{
"name": "greeter",
"version": "1.0.0",
"license": "MIT",
"scripts": { "test": "bun test" },
"devDependencies": { "@types/bun": "latest" }
}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)| File | Holds | Read by |
|---|---|---|
index.js | The bundle, as ES modules | Every runtime and bundler |
index.d.ts | Types for everything index.js exports | Editors, tsc, bun check |
index.aot.ts | index.js with the types of your sources put back | bun compile |
package.json | Your package.json, rewritten for the files above | npm 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.
bun build ./src/index.ts --outdir dist --target node --dtsWhat 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 yetBun 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#
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 missingbun 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.