Skip to main content
Macros are a mechanism for running JavaScript functions at bundle-time. The values returned by these functions are directly inlined into your bundle. As a toy example, consider this simple function that returns a random number.
random.ts
This is just a regular function in a regular file, but we can use it as a macro like so:
cli.tsx
Macros are identified using the import attributes syntax. If you haven’t seen this syntax before, it’s a Stage 3 TC39 proposal that allows you to attach additional metadata to import statements.
Now let’s bundle this file with bun build. The bundled file will be printed to stdout.
As you can see, the source code of the random function doesn’t appear in the bundle. Instead, it was executed at bundle-time, and the function call (random()) was replaced with the result of the function. Because the source code is never included in the bundle, macros can safely perform privileged operations like reading from a database.

When to use macros

If you have several one-off build scripts for small tasks, executing code at bundle-time can be easier to maintain. Macros live alongside other code, run in sync with the build, are automatically parallelized, and fail the build if they fail. However, if you find yourself running lots of code at bundle-time, consider running a server instead.

Import attributes

Bun macros are annotated with import statements using:
  • with { type: 'macro' } — An import attribute, a Stage 3 ECMA Script proposal
  • assert { type: 'macro' } — An import assertion, an older form of import attributes that is now deprecated (but already shipped in multiple browsers and runtimes)

Safety considerations

Macros must be explicitly imported with { type: "macro" } to be executed at bundle-time. Unless you call those macro imports, they have no effect, whereas normal JavaScript imports can have side effects. You can completely disable macros by passing --no-macros to Bun, which will produce a build error like this:
To reduce the potential attack surface of malicious packages, macros cannot be called from inside node_modules/**/*. If a package tries to call a macro, you’ll see an error like this:
Your application code can still import macros from node_modules and call them.
cli.tsx

Export condition “macro”

When publishing a library with macros to npm or other package registries, use the "macro" export condition to provide a special version for the macro environment.
package.json
This configuration allows users to consume your package using the same import specifier at runtime or bundle-time:
The first import resolves to ./node_modules/my-package/index.js, while the second import is resolved by Bun’s bundler to ./node_modules/my-package/index.macro.js.

Execution

When the Bun transpiler encounters a macro import, it uses Bun’s JavaScript runtime to call the function inside the transpiler and convert the JavaScript return value to an AST node. These functions are called at bundle-time, not runtime. Macros execute synchronously in the transpiler’s visit pass—after plugins, before generating the AST. They execute in import order. The transpiler waits for macros to execute before continuing. If a macro returns a Promise, the transpiler will also wait for it to complete. Bun’s bundler is multi-threaded, so macros execute in parallel across multiple JavaScript “worker threads”.

Dead code elimination

After macros execute and are inlined, the bundler performs dead code elimination. For instance, this macro:
returnFalse.ts
Bundling the following code (with syntax minification enabled) will result in an empty bundle:

Serializability

Bun’s transpiler must be able to serialize a macro’s return value to inline it into the AST. All JSON-compatible data structures are supported:
macro.ts
Macros can be async or return Promise instances. Bun’s transpiler will automatically wait for the Promise and inline the result.
macro.ts
The transpiler implements special serialization logic for common data formats like Response, Blob, and TypedArray.
  • TypedArray: Resolved to a base64-encoded string.
  • Response: Bun reads the Content-Type and serializes accordingly; e.g. application/json types are automatically parsed to objects, text/plain is inlined as a string. Unknown or undefined Response types are converted to base64.
  • Blob: Like Response, serialization depends on its type property.
fetch returns a Promise<Response>, which can be returned directly.
macro.ts
Functions and most class instances (except those mentioned above) are not serializable.
macro.ts

Arguments

Macros can accept inputs, but only in limited cases. Argument values must be statically known. For instance, the following usage is not allowed:
If the value of foo is known at bundle-time (e.g. it’s a constant or the result of another macro), then the following usage is allowed:

Type safety

Macros are just regular functions, so they work seamlessly with TypeScript:
random.ts
cli.ts

Examples

Reading files at build time

readFile.ts
app.ts

Environment variables

env.ts
app.ts

Build-time calculations

calculate.ts
app.ts

Fetching data at build time

fetch.ts
app.ts

Generate code from database

db.ts
app.ts

Inline build metadata

build-info.ts
app.ts

Debugging macros

If a macro throws an error, the build will fail with a stack trace:
You can add console.log() statements to debug macros:
macro.ts
These logs will appear in your terminal during the build.

Limitations

  • Macros cannot import files that are not yet processed by the bundler
  • Circular dependencies between macros are not supported
  • Macros cannot use dynamic imports (import())
  • Macro arguments must be statically analyzable
  • Only serializable values can be returned

Performance considerations

Macros run during the build, so expensive operations will slow down your build time:
  • Cache results when possible
  • Avoid unnecessary network requests
  • Consider using build-time constants instead of macros for simple values
  • Use --no-macros in development if macros are slow

Comparison with other approaches

vs. Codegen scripts

Macros run inline with your code, automatically parallelize, and fail the build if they fail. Separate codegen scripts require manual orchestration.

vs. Build-time plugins

Macros are simpler—just import and call functions. Plugins require more setup but offer more control over the build process.

vs. Runtime code

Macros run at build time, so the results are inlined and the source code is not included in the bundle. Runtime code runs every time your app starts.