From 9d68ec882a2efe1e977d99b204fedea39ff97298 Mon Sep 17 00:00:00 2001 From: robobun Date: Sat, 31 Jan 2026 17:35:03 -0800 Subject: [PATCH] require --compile for ESM bytecode (#26624) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 Co-authored-by: Claude Opus 4.5 Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- docs/bundler/bytecode.mdx | 46 +++++++++++------------------------- docs/bundler/executables.mdx | 5 +--- docs/bundler/index.mdx | 28 ++++++++++++++++++++-- docs/snippets/cli/build.mdx | 3 ++- packages/bun-types/bun.d.ts | 5 +++- src/bun.js/api/JSBundler.zig | 7 ++++++ src/cli/Arguments.zig | 18 ++++++++++---- 7 files changed, 67 insertions(+), 45 deletions(-) diff --git a/docs/bundler/bytecode.mdx b/docs/bundler/bytecode.mdx index 748d81eebd..12a9a67875 100644 --- a/docs/bundler/bytecode.mdx +++ b/docs/bundler/bytecode.mdx @@ -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: diff --git a/docs/bundler/executables.mdx b/docs/bundler/executables.mdx index f2f9cd3fb6..4b65ff5fdc 100644 --- a/docs/bundler/executables.mdx +++ b/docs/bundler/executables.mdx @@ -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. - - **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! - +Bytecode compilation supports both `cjs` and `esm` formats when used with `--compile`. ### What do these flags do? diff --git a/docs/bundler/index.mdx b/docs/bundler/index.mdx index 46e4c8ce6d..cba9088253 100644 --- a/docs/bundler/index.mdx +++ b/docs/bundler/index.mdx @@ -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. ```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, + }) ``` + ```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 ``` + @@ -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 diff --git a/docs/snippets/cli/build.mdx b/docs/snippets/cli/build.mdx index 07999eae33..ec3a9d7490 100644 --- a/docs/snippets/cli/build.mdx +++ b/docs/snippets/cli/build.mdx @@ -50,7 +50,8 @@ bun build - Module format of the output bundle. One of esm, cjs, or iife + Module format of the output bundle. One of esm, cjs, or iife. Defaults to{" "} + cjs when --bytecode is used. ### File Naming diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 80a7ec66f7..a3db6771cc 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -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 diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig index 9de645a85c..3d652f3b45 100644 --- a/src/bun.js/api/JSBundler.zig +++ b/src/bun.js/api/JSBundler.zig @@ -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; } diff --git a/src/cli/Arguments.zig b/src/cli/Arguments.zig index 2a9c84c880..5c7ad1c2e8 100644 --- a/src/cli/Arguments.zig +++ b/src/cli/Arguments.zig @@ -179,7 +179,7 @@ pub const build_only_params = [_]ParamType{ clap.parseParam("--sourcemap ? Build with sourcemaps - 'linked', 'inline', 'external', or 'none'") catch unreachable, clap.parseParam("--banner Add a banner to the bundled output such as \"use client\"; for a bundle being used with RSCs") catch unreachable, clap.parseParam("--footer Add a footer to the bundled output such as // built with bun!") catch unreachable, - clap.parseParam("--format Specifies the module format to build to. \"esm\", \"cjs\" and \"iife\" are supported. Defaults to \"esm\".") catch unreachable, + clap.parseParam("--format 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 Root directory used for multiple entry points") catch unreachable, clap.parseParam("--splitting Enable code splitting") catch unreachable, clap.parseParam("--public-path A prefix to be appended to any import paths in bundled code") catch unreachable, @@ -1346,10 +1346,18 @@ 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) { - Output.errGeneric("format must be 'cjs' or 'esm' when bytecode is true.", .{}); - Global.exit(1); + 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); + } } }