Skip to main content
Macros are JavaScript functions that run at bundle-time. Their return values are inlined directly into your bundle. As a toy example, consider this function that returns a random number.
random.ts
This is a regular function in a regular file, but you can use it as a macro:
cli.tsx
Macros are marked with import attribute syntax, a Stage 3 TC39 proposal for attaching additional metadata to import statements.
Bundle the file with bun build. The bundled file is printed to stdout.
terminal
The source code of the random function occurs nowhere in the bundle. Instead, it runs during bundling and the call (random()) is replaced with its result. Since the source code is never included in the bundle, macros can safely perform privileged operations like reading from a database.

When to use macros

For small things you would otherwise write a one-off build script for, bundle-time code execution can be easier to maintain. It lives with the rest of your code, it runs with the rest of the build, it is automatically parallelized, and if it fails, the build fails too. If you find yourself running a lot of code at bundle-time though, consider running a server instead.

Import attributes

Macros are import statements annotated with either:
  • with { type: 'macro' } — an import attribute, a Stage 3 ECMAScript proposal
  • assert { type: 'macro' } — an import assertion, an earlier incarnation of import attributes that has now been abandoned (but is already supported by a number of browsers and runtimes)

Security considerations

Macros must be explicitly imported with { type: "macro" } to run at bundle-time. These imports have no effect if they are not called, unlike regular JavaScript imports which may have side effects. You can disable macros entirely with the --no-macros flag. It produces a build error like this:
To reduce the potential attack surface for malicious packages, macros cannot be invoked from inside node_modules/**/*. If a package attempts to invoke a macro, you’ll see an error like this:
Your application code can still import macros from node_modules and invoke them.
cli.tsx

Export condition “macro”

When shipping a library containing a macro to npm or another package registry, use the "macro" export condition to provide a version of your package exclusively for the macro environment.
package.json
With this configuration, users can consume your package at runtime or at bundle-time using the same import specifier:
index.ts
The first import resolves to ./node_modules/my-package/index.js; Bun’s bundler resolves the second to ./node_modules/my-package/index.macro.js.

Execution

When Bun’s transpiler sees a macro import, it calls the function using Bun’s JavaScript runtime and converts the return value into an AST node. Macros run synchronously in the transpiler during the visiting phase, before plugins and before the transpiler generates the AST. They run in the order they are imported. The transpiler waits for each macro to finish before continuing, and awaits any Promise a macro returns. Bun’s bundler is multi-threaded, so macros execute in parallel in multiple spawned JavaScript “workers”.

Dead code elimination

The bundler performs dead code elimination after running and inlining macros. Given the following macro:
returnFalse.ts
…bundling the following file produces an empty bundle, provided that the minify syntax option is enabled.
index.ts

Serializability

Bun’s transpiler must be able to serialize the result of the macro 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 awaits the Promise and inlines the result.
macro.ts
The transpiler implements special logic for serializing common data formats like Response and Blob.
  • Response: Bun reads the Content-Type and serializes accordingly; for example, a Response with type application/json is parsed into an object and text/plain is inlined as a string. Responses with an unrecognized or undefined type are base64-encoded.
  • Blob: As with Response, the serialization depends on the type property.
The result of fetch is Promise<Response>, so it can be directly returned.
macro.ts
Functions and instances of most classes (except those listed earlier) are not serializable.
macro.ts

Arguments

Macros can accept inputs, but only in limited cases. The value must be statically known. For example, the following is not allowed:
index.ts
However, if the value of foo is known at bundle-time (say, if it’s a constant or the result of another macro), then it’s allowed:
index.ts
This outputs:

Examples

Embed latest git commit hash

getGitCommitHash.ts
When you build it, the getGitCommitHash call is replaced with the result of calling the function:
You’re probably thinking “Why not just use process.env.GIT_COMMIT_HASH?” Well, you can do that too. But can you do this with an environment variable?

Make fetch() requests at bundle-time

This example makes an outgoing HTTP request with fetch(), parses the HTML response with HTMLRewriter, and returns an object containing the title and meta tags, all at bundle-time.
meta.ts
The extractMetaTags function is erased at bundle-time and replaced with the result of the function call: the fetch request happens at bundle-time, and the result is embedded in the bundle. The branch throwing the error is also eliminated since it’s unreachable.