Skip to content

macros: create the build-time macro VM from the build's transform options - #40750

Merged
dylan-conway merged 5 commits into
mainfrom
farm/e5fbf73f/macro-vm-transform-options
Aug 28, 2026
Merged

dylan-conway merged 5 commits into
mainfrom
farm/e5fbf73f/macro-vm-transform-options

Conversation

@robobun

@robobun robobun commented Aug 28, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • bun build --define process.env.MODE:'"prod"' app.ts, where app.ts imports a macro that reads process.env.MODE, inlines "none" (the macro's fallback). A file the macro module imports through --loader .dat:text is parsed as JavaScript instead (Expected ";" but found "from"). Bun.build({ define, loader }) and a bun --define run whose macro sits in a module transpiled off the main thread behave the same way.
  • When no VM is loaded on the current thread (a bundler parse worker, the Bun.build() bundle thread, a runtime transpiler-store worker), Macro::init (src/js_parser_jsc/Macro.rs) creates the macro VM with InitOptions { log, env_loader, is_main_thread, ..Default::default() }. transform_options stays at its default, so the VM's transpiler has no defines, no loaders, no tsconfig override. The Zig version passed .args = resolver.opts.transform_options here.

Fix

  • MacroContext keeps an Arc clone of transpiler.options.transform_options (the field already is an Arc). Macro::init copies it into InitOptions::transform_options when it creates the VM, so the macro module is transpiled with the same --define, --loader, --tsconfig-override, JSX and resolution settings as the build.
  • --external and --packages=external are cleared on that copy. They are build-only flags that describe the output bundle. The macro module runs at build time, so its own import foo from "foo" must still resolve. Without this, bun build -e foo broke a macro that imports foo.
  • Bun.build() now includes its loader map in the TransformOptions it builds (js_bundle_completion_task.rs). configure_bundler already applied the map to the build's own transpiler, but the macro VM is created from the TransformOptions.
  • Verified: test/bundler/bundler_edgecase.test.ts (MacroSeesBuildDefinesAndLoadersCLI, MacroSeesBuildDefinesAndLoadersAPI, MacroImportsPackageMarkedExternal) and test/bundler/transpiler/macro-test.test.ts ("a macro in a module transpiled off the main thread sees --define"). Also ran bundler_edgecase.test.ts, bun-build-api.test.ts, transpiler.test.js, bundler_env.test.ts, bundler_loader.test.ts and the macro regression tests.

Background

  • A macro import (with { type: "macro" }) runs the imported module in a JSC VM at build time and splices the call result into the AST. In bun run the current VM is used. In bun build the parse happens on worker threads that have no VM, so Macro::init creates one per thread.
  • api::TransformOptions is the CLI/bunfig option bag. BundleOptions::from_api projects it into the transpiler's options (defines, loaders, externals, conditions) and keeps the original in BundleOptions::transform_options. VirtualMachine::init takes a TransformOptions by value and builds the VM's transpiler from it.
  • The resolver treats a specifier that matches --external or --packages=external as external and returns it unresolved. The bundler leaves such an import in the output. A runtime VM has no such concept and fails to load the module.
Notes
  • Repro, released 1.4.1: app.ts has import { mode } from "./m.ts" with { type: "macro" }; console.log(mode()); and m.ts has export function mode() { return process.env.MODE ?? "none"; }. bun build --define 'process.env.MODE:"prod"' app.ts prints console.log("none"). bun --define 'process.env.MODE:"prod"' app.ts prints prod (the entry point is transpiled on the main thread). With app.ts importing lib.ts that uses the macro, the run prints none too: lib.ts goes through the runtime transpiler store on a worker thread.
  • Bun.build() with define and a macro printed none as well, and neither the runtime's own --define nor the build's reached the macro: the bundle thread and its workers have no VM.
  • TransformOptions is exposed through bun_bundler::options next to the existing EnvBehavior re-export, so bun_js_parser_jsc does not need a new Cargo dependency.
  • The macro VM runs configure_defines after init, which loads .env files into the shared env loader again. The build's transpiler loaded the same files into the same loader first, so this is a repeat of the same work, unchanged from before.
  • --drop, --feature, --conditions, --main-fields, --extension-order, --preserve-symlinks, --env-file and the JSX flags are accepted by bun run too and now reach the macro VM the same way they reach a runtime VM. --external and --packages are build-only and are cleared.
  • The two bytecode tests in bun-build-api.test.ts (function record on an encoder page boundary, repeated builds don't retain the generated code) hit the 5 s timeout on an unmodified main debug build in this container as well. Unrelated.

no test proof · iteration 3 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/bundler/bundler_edgecase.test.ts

…ions

When no VM is loaded on the current thread (a `bun build` parse worker,
the `Bun.build()` bundle thread, or a runtime transpiler-store worker),
`Macro::init` creates a VM to run the macro module in. It left
`InitOptions::transform_options` at its default, so the macro module was
transpiled without the build's `--define`, `--loader`, tsconfig override,
JSX settings and the rest of the transform options.

`MacroContext` now keeps an `Arc` of `transpiler.options.transform_options`
and `Macro::init` passes a copy of it when it creates the VM. `--external`
and `--packages=external` are cleared on that copy: they describe the
output bundle, and the macro module's own imports must still resolve at
build time.

`Bun.build()` also puts its `loader` map into the `TransformOptions` it
builds, so the macro VM it creates sees the same loaders.
@coderabbitai

coderabbitai Bot commented Aug 28, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

On-demand reviews are free for the next 23 days. After that, they cost $0.25 per reviewed file.

Or wait 18 minutes for your next included review.

View limit details

Limit details: You’ve used all 5 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 509e710a-9a17-4a8d-a63a-99113e120258

📥 Commits

Reviewing files that changed from the base of the PR and between f170b9c and 6e1fb5e.

📒 Files selected for processing (5)
  • src/bundler/lib.rs
  • src/js_parser_jsc/Macro.rs
  • src/runtime/api/js_bundle_completion_task.rs
  • test/bundler/bundler_edgecase.test.ts
  • test/bundler/transpiler/macro-test.test.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Aug 28, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 2:49 AM PT - Aug 28th, 2026

❌ @robobun, your commit 6e1fb5e has 1 failures in Build #107588 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 40750

That installs a local version of the PR into your bun-40750 executable, so you can run:

bun-40750 --bun

Comment thread src/js_parser_jsc/Macro.rs
`Bun.build()` parses on the process-wide worker pool. The macro VM a
worker creates takes the options of the build that creates it and stays
alive after that build. In bundler_edgecase.test.ts an earlier in-process
build could create that VM first, so the API variant of the test was
order dependent. It now spawns a fresh process from macro-test.test.ts.
Comment thread src/js_parser_jsc/Macro.rs Outdated
Comment thread src/js_parser_jsc/Macro.rs Outdated
Comment thread src/js_parser_jsc/Macro.rs Outdated
Comment thread src/runtime/api/js_bundle_completion_task.rs Outdated
Comment thread src/js_parser_jsc/Macro.rs Outdated
Comment thread src/js_parser_jsc/Macro.rs Outdated
Comment thread src/js_parser_jsc/Macro.rs

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

@robobun

robobun commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

CI state at 6e1fb5e: the new tests pass on every lane. The only test that is red after retries is test/js/web/url/url.test.ts on darwin x64 (TypeError: Invalid URL). It fails on main too and is with main-break triage. The rest of the red lanes passed on retry or when run alone (require-cache.test.ts, napi.test.ts, tcp-server.test.ts, serve-http2.test.ts, and others), none of them near this change. Ready for review.

@dylan-conway
dylan-conway merged commit 88b0fb8 into main Aug 28, 2026
9 of 10 checks passed
@dylan-conway
dylan-conway deleted the farm/e5fbf73f/macro-vm-transform-options branch August 28, 2026 15:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants