new·The score now tells you which way it movedA brain's exam only ever grows: its own material writes questions, and so does every question a real caller asked and did not get answered. The score is a percentage over that growing set, so a brain that learned more could post a smaller number — and this week three did. One of them answered two MORE questions than the week before and showed eighteen points less. Printed as a single percentage, that reads as decline to a reader and as punishment to anyone who contributes material.all news →
mozg.beta
Sign in

Bun · Bundler · all subjects

macros

29 notes, read out of this brain and free to use. Each one was extracted from a source and is re-checked against its exam.

Check for HMR availability with import.meta.hot

You can check for HMR API availability by testing if (import.meta.hot), which tree-shakes in production. Bun dead-code-eliminates calls to all HMR APIs in production builds.

HMR APIs must be called without indirection for dead-code elimination

For dead-code elimination to work, Bun forces HMR APIs to be called without indirection. The full phrase 'import.meta.hot.<API>' must be called directly. Invalid patterns include: assigning hot to a variable, assigning import.meta to a variable, or passing import.meta.hot.dispose to a function. Exception: data can be passed to functions (doSomething(import.meta.hot.data) is valid).

import.meta.hot API methods reference

The import.meta.hot API provides the following methods: hot.accept() (✅ Indicate that a hot update can be replaced gracefully), hot.data (✅ Persist data between module evaluations), hot.dispose() (✅ Add a callback function to run when a module is about to be replaced), hot.invalidate() (❌ not implemented), hot.on() (✅ Attach an event listener), hot.off() (✅ Remove an event listener from on), hot.send() (❌ not implemented), hot.prune() (🚧 Callback is currently never called), hot.decline() (✅ No-op to match Vite's import.meta.hot).

hot.accept() without arguments creates a hot-reloading boundary

Called without arguments, import.meta.hot.accept() means Bun can replace the module by re-evaluating the file. This creates a hot-reloading boundary for all files that the current module imports. Whenever a dependency is saved, the update bubbles up and the file re-evaluates. Bun then patches the files that import the current module. If only the current module is updated, Bun re-evaluates only that file.

hot.accept() with callback instead of patching importers

When passed a callback, import.meta.hot.accept(newModule => { ... }) calls the callback with the new module instead of patching the importers. The newModule parameter is undefined when a SyntaxError occurred. Bun recommends prefer using import.meta.hot.accept() without an argument as it usually makes code clearer.

hot.accept() with single dependency

You can accept a specific dependency by passing the dependency path: import.meta.hot.accept('./foo', newModule => { ... }). When the dependency is updated, Bun calls the callback with the new module.

hot.accept() with multiple dependencies

You can accept multiple dependencies by passing an array: import.meta.hot.accept(['./foo', './bar'], newModules => { ... }). The callback receives an array with the updated module at its index and undefined for the other dependencies.

hot.data persists state across module replacements

import.meta.hot.data carries state from the previous version of a module to the new one across a hot replacement. Writing to import.meta.hot.data also marks the module as self-accepting (equivalent to calling import.meta.hot.accept()). In production, Bun inlines data as {}, so it cannot be used as a state holder. Bun can minify {}.prop ??= value into value in production.

hot.dispose() callback timing

import.meta.hot.dispose() attaches an on-dispose callback that Bun calls just before the module is replaced with another copy (before the next is loaded) and after the module is detached (removing all imports to this module). Returning a promise delays module replacement until the module is disposed. Bun calls all dispose callbacks in parallel. Bun does not call this callback on route navigation or when the browser tab closes.

hot.prune() for cleaning up when module imports are removed

import.meta.hot.prune() attaches an on-prune callback that Bun calls when all imports to the module are removed, but the module was previously loaded. It is used to clean up resources created when the module was loaded. Unlike hot.dispose(), it pairs better with accept and data for managing stateful resources. Note: Callback is currently never called.

hot.on() and hot.off() for HMR events

import.meta.hot.on(eventName, callback) attaches an event listener. import.meta.hot.off(eventName, callback) removes an event listener. Event names carry a prefix so plugins do not conflict with each other. When a file is replaced, Bun automatically removes all of its event listeners.

Built-in HMR events

Built-in HMR events are: bun:beforeUpdate (before a hot update is applied), bun:afterUpdate (after a hot update is applied), bun:beforeFullReload (before a full page reload happens), bun:beforePrune (before prune callbacks are called), bun:invalidate (when a module is invalidated with import.meta.hot.invalidate()), bun:error (when a build or runtime error occurs), bun:ws:disconnect (when the HMR WebSocket connection is lost, indicating the development server is offline), bun:ws:connect (when the HMR WebSocket connects or re-connects). For Vite compatibility, these events are also available with the vite:* prefix instead of bun:*.

Page reload when no modules accept hot updates

When no modules call import.meta.hot.accept() and there isn't React Fast Refresh or a plugin calling it, the page reloads when the file updates. A console warning shows which files were invalidated. This warning is safe to ignore if it makes more sense to rely on full page reloads.

Non-serializable macro return types

Functions and instances of most classes (except Response and Blob) are not serializable and cannot be returned from macros.

Macros: definition and bundle-time execution

Macros are JavaScript functions that run at bundle-time. Bun inlines their return values directly into the bundle. The function source code does not appear in the final bundle; instead, the function executes during bundling and Bun replaces the call with its result.

Macro import syntax with import attributes

Macros are marked with import attribute syntax: import { functionName } from "./file.ts" with { type: "macro" }. Alternatively, the deprecated import assertion syntax can be used: assert { type: "macro" }.

Macros cannot be invoked from node_modules

Code inside node_modules/**/* cannot invoke macros. If a package in node_modules attempts to invoke a macro, a build error is produced. However, application code can import macros from node_modules and invoke them from outside node_modules.

Macro export condition in package.json

Libraries can use the "macro" export condition in package.json to provide a version exclusively for the macro environment. Example: { "exports": { "import": "./index.js", "macro": "./index.macro.js" } }

Macro execution timing and order

Macros run synchronously in the transpiler during the visiting phase, after the transpiler parses the file into an AST. They execute in the order their calls appear in the file. The transpiler does not load or run a macro module until it reaches a call to one of its exports. The transpiler waits for each macro to finish before continuing and awaits any Promise a macro returns.

Macros execute in parallel across threads

Bun's bundler is multi-threaded, so macros execute in parallel in multiple spawned JavaScript workers.

Dead code elimination after macro inlining

The bundler performs dead code elimination after running and inlining macros. Code branches dependent on macro return values can be eliminated if unreachable, provided that the minify syntax option is enabled.

Macro serializability: JSON-compatible data

Bun's transpiler can serialize JSON-compatible data structures returned by macros: objects, arrays, strings, numbers, booleans, and nested combinations thereof.

Async macros and Promise support

Macros can be async or return Promise instances. Bun's transpiler awaits the Promise and inlines the resolved result.

Macro serialization of Response objects

When a macro returns a Response, Bun reads the Content-Type header and serializes accordingly. For application/json, Bun parses the Response into an object. For text/plain, Bun inlines as a string. For unrecognized or undefined types, Bun base64-encodes the Response.

Macro serialization of Blob objects

When a macro returns a Blob, Bun serializes based on the type property, similar to Response serialization.

Fetch in macros returns Promise<Response>

The result of fetch() is Promise<Response>, so a macro can return a fetch() call directly and Bun will handle the serialization.

Macro arguments must be statically known

Macros can only accept arguments whose values are statically known at bundle-time. Values from runtime operations like Math.random() cannot be used. Values that are constants or results of other macros are allowed.

Macro example: embed git commit hash

export function getGitCommitHash() { const { stdout } = Bun.spawnSync({ cmd: ["git", "rev-parse", "HEAD"], stdout: "pipe", }); return stdout.toString(); } This example calls git at bundle-time and embeds the commit hash directly in the bundle.

Macro example: extract meta tags at bundle-time

export async function extractMetaTags(url: string) { const response = await fetch(url); const meta = { title: "", }; new HTMLRewriter() .on("title", { text(element) { meta.title += element.text; }, }) .on("meta", { element(element) { const name = element.getAttribute("name") || element.getAttribute("property") || element.getAttribute("itemprop"); if (name) meta[name] = element.getAttribute("content"); }, }) .transform(response); return meta; } This example fetches a webpage and extracts meta tags at bundle-time, embedding the result in the final bundle.

Give your agent this brain