Macros are a mechanism for running JavaScript functions at bundle time. The return value of each function is serialized and inlined directly into the bundled output — the macro’s source code never appears in the final bundle.
Basic example
Consider a function that returns a random number:
export function random() {
return Math.random();
}
Use it as a macro with the with { type: "macro" } import attribute:
import { random } from "./random.ts" with { type: "macro" };
console.log(`Your random number is ${random()}`);
Build it:
The macro runs at bundle time. The output contains the result, not the function:
// Output
console.log(`Your random number is ${0.6805550949689833}`);
The source code of random.ts does not appear in the bundle at all.
Macros use import attribute syntax — a Stage 3 TC39 proposal for attaching metadata to import statements. Both with { type: "macro" } and the older assert { type: "macro" } are supported.
When to use macros
Macros are useful when you want to:
- Embed build-time constants — version numbers, git commit hashes, build timestamps
- Fetch external data at build time — CMS content, API responses, feature flags
- Generate code from schemas — introspect a database, generate typed clients
- Replace one-off build scripts — macros live alongside your source code, run in parallel with the build, and fail the build if they throw
If you’re running a large amount of code at build time, consider a server instead. Macros are not a general-purpose build system.
Real-world examples
Embed the current git commit hash
export function getGitHash() {
const { stdout } = Bun.spawnSync({
cmd: ["git", "rev-parse", "HEAD"],
stdout: "pipe",
});
return stdout.toString().trim();
}
import { getGitHash } from "./getGitHash.ts" with { type: "macro" };
console.log(`Version: ${getGitHash()}`);
Fetch data at build time
In this example, an HTTP request happens at bundle time and the result is inlined into the bundle. The fetch call does not appear in the output:
export async function extractMetaTags(url: string) {
const response = await fetch(url);
const meta: Record<string, string> = { title: "" };
new HTMLRewriter()
.on("title", {
text(el) { meta.title += el.text; },
})
.on("meta", {
element(el) {
const name = el.getAttribute("name") || el.getAttribute("property");
if (name) meta[name] = el.getAttribute("content") ?? "";
},
})
.transform(response);
return meta;
}
import { extractMetaTags } from "./meta.ts" with { type: "macro" };
export const Head = () => {
const tags = extractMetaTags("https://example.com");
if (tags.title !== "Example Domain") {
throw new Error("Unexpected title");
}
return (
<head>
<title>{tags.title}</title>
<meta name="viewport" content={tags.viewport} />
</head>
);
};
The error branch is also eliminated because the condition is now statically false.
Serializability
Macros must return JSON-serializable values. Bun also handles these special types:
| Return type | Serialization |
|---|
string, number, boolean, null, object, array | Inlined as-is |
Promise | Awaited automatically; result must be serializable |
Response with application/json | Parsed to object |
Response with text/plain | Inlined as string |
Response with other type | Base64-encoded string |
Blob | Depends on type property (same as Response) |
TypedArray (e.g. Uint8Array) | Base64-encoded string |
function, class instance | Not supported — throws an error |
Macros can be async. The transpiler awaits the returned promise automatically:
export async function getConfig() {
const text = await Bun.file("./config.json").text();
return JSON.parse(text);
}
Arguments
Macro arguments must be statically known at bundle time. Dynamic values are not allowed:
import { getText } from "./getText.ts" with { type: "macro" };
// This is NOT allowed — foo is dynamic
const foo = Math.random() ? "foo" : "bar";
const text = getText(`https://example.com/${foo}`);
If the value is statically known — for example, a constant or the result of another macro — it is allowed:
import { getText } from "./getText.ts" with { type: "macro" };
import { getBaseUrl } from "./getBaseUrl.ts" with { type: "macro" };
// getBaseUrl() is statically known (runs at bundle time)
const text = getText(`${getBaseUrl()}/content`);
console.log("Length:", text.length);
Dead code elimination
Macros run before dead code elimination. If a macro returns false, the branch that depends on it is removed from the output (when --minify-syntax is enabled):
export function returnFalse() {
return false;
}
import { returnFalse } from "./returnFalse.ts" with { type: "macro" };
if (returnFalse()) {
console.log("This is eliminated");
}
bun build ./index.ts --minify-syntax
# Output: (empty bundle)
Execution model
Macros are executed by Bun’s JavaScript runtime inside the transpiler, during the visiting phase — before plugins and before the AST is generated. Key behaviors:
- Macros execute in the order their imports appear
- The transpiler waits for each macro to finish (including async macros)
- Bun’s bundler is multi-threaded: macros execute in parallel across worker threads
- Macros run in a fully sandboxed Bun environment with access to all Bun and Node.js APIs
Security
Macros must be explicitly imported with with { type: "macro" } before they can run. Unused macro imports have no effect.
Disable macros entirely with --no-macros:
bun build ./index.ts --no-macros
error: Macros are disabled
foo();
^
./hello.js:3:1
Macros cannot run from node_modules. If a package tries to invoke a macro internally, the build fails:
error: For security reasons, macros cannot be run from node_modules.
Your code can still import a macro from a package and invoke it:
import { macro } from "some-package" with { type: "macro" };
macro(); // allowed — your code is invoking it
Publishing macros in npm packages
Use the "macro" export condition to ship a macro-specific version of your package:
{
"name": "my-package",
"exports": {
"import": "./index.js",
"macro": "./index.macro.js"
}
}
Users can then import the same package at runtime or as a macro using the same specifier:
import pkg from "my-package"; // runtime import → index.js
import { build } from "my-package" with { type: "macro" }; // macro import → index.macro.js