mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
require --compile for ESM bytecode (#26624)
## Summary - Add validation to require `--compile` when using ESM bytecode - Update documentation to clarify ESM bytecode requirements ## Why ESM module resolution is two-phase: (1) analyze imports/exports, (2) evaluate. Without `--compile`, there's no `module_info` embedded, so JSC must still parse the file for module analysis even with bytecode - causing a double-parse deopt. ## Changes - **CLI**: Error when `--bytecode --format=esm` is used without `--compile` - **JS API**: Error when `bytecode: true, format: 'esm'` is used without `compile: true` - **Docs**: Update bytecode.mdx, executables.mdx, index.mdx to clarify requirements - **Types**: Update JSDoc for bytecode option in bun.d.ts ## Test plan ```bash # Should error bun build ./test.js --bytecode --format=esm --outdir=./out # error: ESM bytecode requires --compile. Use --format=cjs for bytecode without --compile. # Should work bun build ./test.js --bytecode --format=esm --compile --outfile=./mytest bun build ./test.js --bytecode --format=cjs --outdir=./out ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -7,9 +7,9 @@ Bytecode caching is a build-time optimization that dramatically improves applica
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic usage
|
||||
### Basic usage (CommonJS)
|
||||
|
||||
Enable bytecode caching with the `--bytecode` flag:
|
||||
Enable bytecode caching with the `--bytecode` flag. Without `--format`, this defaults to CommonJS:
|
||||
|
||||
```bash terminal icon="terminal"
|
||||
bun build ./index.ts --target=bun --bytecode --outdir=./dist
|
||||
@@ -17,7 +17,7 @@ bun build ./index.ts --target=bun --bytecode --outdir=./dist
|
||||
|
||||
This generates two files:
|
||||
|
||||
- `dist/index.js` - Your bundled JavaScript
|
||||
- `dist/index.js` - Your bundled JavaScript (CommonJS)
|
||||
- `dist/index.jsc` - The bytecode cache file
|
||||
|
||||
At runtime, Bun automatically detects and uses the `.jsc` file:
|
||||
@@ -28,14 +28,24 @@ bun ./dist/index.js # Automatically uses index.jsc
|
||||
|
||||
### With standalone executables
|
||||
|
||||
When creating executables with `--compile`, bytecode is embedded into the binary:
|
||||
When creating executables with `--compile`, bytecode is embedded into the binary. Both ESM and CommonJS formats are supported:
|
||||
|
||||
```bash terminal icon="terminal"
|
||||
# ESM (requires --compile)
|
||||
bun build ./cli.ts --compile --bytecode --format=esm --outfile=mycli
|
||||
|
||||
# CommonJS (works with or without --compile)
|
||||
bun build ./cli.ts --compile --bytecode --outfile=mycli
|
||||
```
|
||||
|
||||
The resulting executable contains both the code and bytecode, giving you maximum performance in a single file.
|
||||
|
||||
### ESM bytecode
|
||||
|
||||
ESM bytecode requires `--compile` because Bun embeds module metadata (import/export information) in the compiled binary. This metadata allows the JavaScript engine to skip parsing entirely at runtime.
|
||||
|
||||
Without `--compile`, ESM bytecode would still require parsing the source to analyze module dependencies—defeating the purpose of bytecode caching.
|
||||
|
||||
### Combining with other optimizations
|
||||
|
||||
Bytecode works great with minification and source maps:
|
||||
@@ -90,35 +100,9 @@ Larger applications benefit more because they have more code to parse.
|
||||
- ❌ **Code that runs once**
|
||||
- ❌ **Development builds**
|
||||
- ❌ **Size-constrained environments**
|
||||
- ❌ **Code with top-level await** (not supported)
|
||||
|
||||
## Limitations
|
||||
|
||||
### CommonJS only
|
||||
|
||||
Bytecode caching currently works with CommonJS output format. Bun's bundler automatically converts most ESM code to CommonJS, but **top-level await** is the exception:
|
||||
|
||||
```js
|
||||
// This prevents bytecode caching
|
||||
const data = await fetch("https://api.example.com");
|
||||
export default data;
|
||||
```
|
||||
|
||||
**Why**: Top-level await requires async module evaluation, which can't be represented in CommonJS. The module graph becomes asynchronous, and the CommonJS wrapper function model breaks down.
|
||||
|
||||
**Workaround**: Move async initialization into a function:
|
||||
|
||||
```js
|
||||
async function init() {
|
||||
const data = await fetch("https://api.example.com");
|
||||
return data;
|
||||
}
|
||||
|
||||
export default init;
|
||||
```
|
||||
|
||||
Now the module exports a function that the consumer can await when needed.
|
||||
|
||||
### Version compatibility
|
||||
|
||||
Bytecode is **not portable across Bun versions**. The bytecode format is tied to JavaScriptCore's internal representation, which changes between versions.
|
||||
@@ -236,8 +220,6 @@ It's normal for it it to log a cache miss multiple times since Bun doesn't curre
|
||||
- Compressing `.jsc` files for network transfer (gzip/brotli)
|
||||
- Evaluating if the startup performance gain is worth the size increase
|
||||
|
||||
**Top-level await**: Not supported. Refactor to use async initialization functions.
|
||||
|
||||
## What is bytecode?
|
||||
|
||||
When you run JavaScript, the JavaScript engine doesn't execute your source code directly. Instead, it goes through several steps:
|
||||
|
||||
@@ -322,10 +322,7 @@ Using bytecode compilation, `tsc` starts 2x faster:
|
||||
|
||||
Bytecode compilation moves parsing overhead for large input files from runtime to bundle time. Your app starts faster, in exchange for making the `bun build` command a little slower. It doesn't obscure source code.
|
||||
|
||||
<Warning>
|
||||
**Experimental:** Bytecode compilation is an experimental feature. Only `cjs` format is supported (which means no
|
||||
top-level-await). Let us know if you run into any issues!
|
||||
</Warning>
|
||||
<Note>Bytecode compilation supports both `cjs` and `esm` formats when used with `--compile`.</Note>
|
||||
|
||||
### What do these flags do?
|
||||
|
||||
|
||||
@@ -1508,22 +1508,43 @@ BuildArtifact (entry-point) {
|
||||
|
||||
## Bytecode
|
||||
|
||||
The `bytecode: boolean` option can be used to generate bytecode for any JavaScript/TypeScript entrypoints. This can greatly improve startup times for large applications. Only supported for `"cjs"` format, only supports `"target": "bun"` and dependent on a matching version of Bun. This adds a corresponding `.jsc` file for each entrypoint.
|
||||
The `bytecode: boolean` option can be used to generate bytecode for any JavaScript/TypeScript entrypoints. This can greatly improve startup times for large applications. Requires `"target": "bun"` and is dependent on a matching version of Bun.
|
||||
|
||||
- **CommonJS**: Works with or without `compile: true`. Generates a `.jsc` file alongside each entrypoint.
|
||||
- **ESM**: Requires `compile: true`. Bytecode and module metadata are embedded in the standalone executable.
|
||||
|
||||
Without an explicit `format`, bytecode defaults to CommonJS.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="JavaScript">
|
||||
```ts title="build.ts" icon="/icons/typescript.svg"
|
||||
// CommonJS bytecode (generates .jsc files)
|
||||
await Bun.build({
|
||||
entrypoints: ["./index.tsx"],
|
||||
outdir: "./out",
|
||||
bytecode: true,
|
||||
})
|
||||
|
||||
// ESM bytecode (requires compile)
|
||||
await Bun.build({
|
||||
entrypoints: ["./index.tsx"],
|
||||
outfile: "./mycli",
|
||||
bytecode: true,
|
||||
format: "esm",
|
||||
compile: true,
|
||||
})
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="CLI">
|
||||
```bash terminal icon="terminal"
|
||||
# CommonJS bytecode
|
||||
bun build ./index.tsx --outdir ./out --bytecode
|
||||
|
||||
# ESM bytecode (requires --compile)
|
||||
bun build ./index.tsx --outfile ./mycli --bytecode --format=esm --compile
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -1690,7 +1711,10 @@ interface BuildConfig {
|
||||
* start times, but will make the final output larger and slightly increase
|
||||
* memory usage.
|
||||
*
|
||||
* Bytecode is currently only supported for CommonJS (`format: "cjs"`).
|
||||
* - CommonJS: works with or without `compile: true`
|
||||
* - ESM: requires `compile: true`
|
||||
*
|
||||
* Without an explicit `format`, defaults to CommonJS.
|
||||
*
|
||||
* Must be `target: "bun"`
|
||||
* @default false
|
||||
|
||||
@@ -50,7 +50,8 @@ bun build <entry points>
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="--format" type="string" default="esm">
|
||||
Module format of the output bundle. One of <code>esm</code>, <code>cjs</code>, or <code>iife</code>
|
||||
Module format of the output bundle. One of <code>esm</code>, <code>cjs</code>, or <code>iife</code>. Defaults to{" "}
|
||||
<code>cjs</code> when <code>--bytecode</code> is used.
|
||||
</ParamField>
|
||||
|
||||
### File Naming
|
||||
|
||||
5
packages/bun-types/bun.d.ts
vendored
5
packages/bun-types/bun.d.ts
vendored
@@ -2594,7 +2594,10 @@ declare module "bun" {
|
||||
* start times, but will make the final output larger and slightly increase
|
||||
* memory usage.
|
||||
*
|
||||
* Bytecode is currently only supported for CommonJS (`format: "cjs"`).
|
||||
* - CommonJS: works with or without `compile: true`
|
||||
* - ESM: requires `compile: true`
|
||||
*
|
||||
* Without an explicit `format`, defaults to CommonJS.
|
||||
*
|
||||
* Must be `target: "bun"`
|
||||
* @default false
|
||||
|
||||
@@ -1019,6 +1019,13 @@ pub const JSBundler = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// ESM bytecode requires compile because module_info (import/export metadata)
|
||||
// is only available in compiled binaries. Without it, JSC must parse the file
|
||||
// twice (once for module analysis, once for bytecode), which is a deopt.
|
||||
if (this.bytecode and this.format == .esm and this.compile == null) {
|
||||
return globalThis.throwInvalidArguments("ESM bytecode requires compile: true. Use format: 'cjs' for bytecode without compile.", .{});
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -179,7 +179,7 @@ pub const build_only_params = [_]ParamType{
|
||||
clap.parseParam("--sourcemap <STR>? Build with sourcemaps - 'linked', 'inline', 'external', or 'none'") catch unreachable,
|
||||
clap.parseParam("--banner <STR> Add a banner to the bundled output such as \"use client\"; for a bundle being used with RSCs") catch unreachable,
|
||||
clap.parseParam("--footer <STR> Add a footer to the bundled output such as // built with bun!") catch unreachable,
|
||||
clap.parseParam("--format <STR> Specifies the module format to build to. \"esm\", \"cjs\" and \"iife\" are supported. Defaults to \"esm\".") catch unreachable,
|
||||
clap.parseParam("--format <STR> Specifies the module format to build to. \"esm\", \"cjs\" and \"iife\" are supported. Defaults to \"esm\", or \"cjs\" with --bytecode.") catch unreachable,
|
||||
clap.parseParam("--root <STR> Root directory used for multiple entry points") catch unreachable,
|
||||
clap.parseParam("--splitting Enable code splitting") catch unreachable,
|
||||
clap.parseParam("--public-path <STR> A prefix to be appended to any import paths in bundled code") catch unreachable,
|
||||
@@ -1346,11 +1346,19 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
|
||||
}
|
||||
|
||||
ctx.bundler_options.output_format = format;
|
||||
// ESM bytecode is supported for --compile builds (module_info is embedded in binary)
|
||||
if (format != .cjs and format != .esm and ctx.bundler_options.bytecode) {
|
||||
if (ctx.bundler_options.bytecode) {
|
||||
if (format != .cjs and format != .esm) {
|
||||
Output.errGeneric("format must be 'cjs' or 'esm' when bytecode is true.", .{});
|
||||
Global.exit(1);
|
||||
}
|
||||
// ESM bytecode requires --compile because module_info (import/export metadata)
|
||||
// is only available in compiled binaries. Without it, JSC must parse the file
|
||||
// twice (once for module analysis, once for bytecode), which is a deopt.
|
||||
if (format == .esm and !ctx.bundler_options.compile) {
|
||||
Output.errGeneric("ESM bytecode requires --compile. Use --format=cjs for bytecode without --compile.", .{});
|
||||
Global.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (args.flag("--splitting")) {
|
||||
|
||||
Reference in New Issue
Block a user