Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/bundler/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ pub mod options {
/// `options.Format` — many ported call-sites spell this `OutputFormat`.
pub use bun_options_types::Format as OutputFormat;
pub use bun_options_types::schema::api::DotEnvBehavior as EnvBehavior;
/// The type behind `TransformOptions::target`.
pub use bun_options_types::schema::api::Target as TransformTarget;
/// The type behind `BundleOptions::transform_options`.
pub use bun_options_types::schema::api::TransformOptions;

Expand Down
50 changes: 35 additions & 15 deletions src/bundler/transpiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -726,21 +726,7 @@ impl<'a> Transpiler<'a> {
);

if auto_jsx {
// Most of the time, this will already be cached
let top_level_dir = self.fs().top_level_dir;
if let Ok(Some(root_dir)) = self.resolver.read_dir_info(top_level_dir) {
if let Some(tsconfig) = root_dir.tsconfig_json() {
// If we don't explicitly pass JSX, try to get it from the root tsconfig
if self.options.transform_options.jsx.is_none() {
self.options.jsx = jsx_pragma_from_resolver(&tsconfig.jsx);
}
self.options.emit_decorator_metadata = tsconfig.emit_decorator_metadata;
self.options.experimental_decorators = tsconfig.experimental_decorators;
if let Some(v) = tsconfig.use_define_for_class_fields {
self.options.use_define_for_class_fields = v;
}
}
}
self.apply_root_tsconfig();
}
}

Expand All @@ -750,6 +736,40 @@ impl<'a> Transpiler<'a> {
self.configure_linker_with_auto_jsx(true);
}

/// Root `tsconfig.json`: JSX pragma (unless the options set one), decorators, class fields.
fn apply_root_tsconfig(&mut self) {
// Most of the time, this will already be cached
let top_level_dir = self.fs().top_level_dir;
if let Ok(Some(root_dir)) = self.resolver.read_dir_info(top_level_dir) {
if let Some(tsconfig) = root_dir.tsconfig_json() {
// If we don't explicitly pass JSX, try to get it from the root tsconfig
if self.options.transform_options.jsx.is_none() {
self.options.jsx = jsx_pragma_from_resolver(&tsconfig.jsx);
}
self.options.emit_decorator_metadata = tsconfig.emit_decorator_metadata;
self.options.experimental_decorators = tsconfig.experimental_decorators;
if let Some(v) = tsconfig.use_define_for_class_fields {
self.options.use_define_for_class_fields = v;
}
}
}
}

/// Rebuilds `options` and the resolver's copy from `opts`. Call `configure_defines` after.
pub fn reset_transform_options(&mut self, opts: api::TransformOptions) -> crate::Result<()> {
// It copied the old options and env loader; the next parse makes a new one.
if let Some(ctx) = self.macro_context.take() {
ctx.deinit();
}
// `from_api` leaves this at its default; the VM path applies it.
let preserve_symlinks = opts.preserve_symlinks.unwrap_or(false);
self.options = options::BundleOptions::from_api(self.fs_mut(), self.log, opts)?;
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
self.options.preserve_symlinks = preserve_symlinks;
self.sync_resolver_opts();
self.apply_root_tsconfig();
Ok(())
}

/// Load `.env` files into the env loader according to
/// `options.env.behavior`.
pub fn run_env_loader(&mut self, skip_default_env: bool) -> crate::Result<()> {
Expand Down
88 changes: 67 additions & 21 deletions src/js_parser_jsc/Macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::sync::Arc;
use bun_ast::DisableStoreReset;
use bun_ast::{E, Expr, ExprData, ExprNodeList, G, ToJSError};
use bun_ast::{Log, Range, Source};
use bun_bundler::options::TransformOptions;
use bun_bundler::options::{TransformOptions, TransformTarget};
use bun_bundler::{Transpiler, entry_points::MacroEntryPoint};
use bun_collections::{ArrayHashMap, HashMap};
use bun_core::Output;
Expand Down Expand Up @@ -182,16 +182,21 @@ impl MacroContext {
);

let macro_entry = self.macros.get_or_put(hash).expect("unreachable");
if !macro_entry.found_existing {
*macro_entry.value_ptr = match Macro::init(
// Loaded before another build moved this VM (`serve_macro_build`): that module graph is gone.
let stale = macro_entry.found_existing
&& !macro_entry.value_ptr.disabled
&& macro_entry.value_ptr.generation != VirtualMachine::get().macro_options_generation;
if !macro_entry.found_existing || stale {
let loaded = Macro::init(
input_specifier,
log,
self.env,
&self.transform_options,
function_name,
&specifier_buf[0..specifier_buf_len as usize],
hash,
) {
);
drain_macro_vm_log(log);
*macro_entry.value_ptr = match loaded {
Ok(m) => m,
Err(e) => {
*macro_entry.value_ptr = Macro::disabled_sentinel();
Expand Down Expand Up @@ -243,11 +248,29 @@ impl MacroContext {
javascript_object,
)
});
drain_macro_vm_log(log);
Ok(ret?)
// this.macros.getOrPut(key: K)
}
}

/// Moves what a macro VM logged into the build's `log`; the VM outlives the build, so it has its own.
fn drain_macro_vm_log(log: &mut Log) {
if !VirtualMachine::is_loaded() {
return;
}
let vm = VirtualMachine::get();
if vm.macro_build_options.is_none() {
return;
}
if let Some(vm_log) = vm.log_mut() {
if !vm_log.msgs.is_empty() {
vm_log.append_to_with_recycled(log, true);
}
vm_log.reset();
}
}

// ══════════════════════════════════════════════════════════════════════════
// Lower-tier bridge (`bun_ast::Macro::MacroContext` ⇆ this crate)
//
Expand Down Expand Up @@ -380,6 +403,8 @@ fn __bun_macro_context_get_remap(
// is checked before any access (see `MacroContext::call`).
pub struct Macro {
pub(crate) vm: Option<NonNull<VirtualMachine>>,
/// `VirtualMachine::macro_options_generation` when the macro was loaded.
pub(crate) generation: u32,

pub(crate) disabled: bool,
}
Expand All @@ -390,12 +415,25 @@ impl Default for Macro {
}
}

/// The build's options as the macro VM's transpiler takes them.
fn macro_vm_transform_options(build: &TransformOptions) -> TransformOptions {
let mut transform_options = build.clone();
// Build-only flags about the output bundle; the macro module's own imports must still resolve.
transform_options.external = Vec::new();
transform_options.packages = None;
// What `init_runtime_state` forces for every VM.
transform_options.write = Some(false);
transform_options.target = Some(TransformTarget::Bun);
transform_options
}

impl Macro {
/// Sentinel stored in the `MacroMap` when `Macro::init` fails, so subsequent
/// calls with the same hash short-circuit instead of retrying the load.
fn disabled_sentinel() -> Self {
Macro {
vm: None,
generation: 0,
disabled: true,
}
}
Expand All @@ -413,33 +451,39 @@ impl Macro {

pub(crate) fn init(
input_specifier: &[u8],
log: &mut Log,
env: *mut DotEnvLoader,
transform_options: &TransformOptions,
build_options: &Arc<TransformOptions>,
function_name: &[u8],
specifier: &[u8],
hash: i32,
) -> crate::Result<Macro> {
let (vm, is_new_vm): (*mut VirtualMachine, bool) = if VirtualMachine::is_loaded() {
(VirtualMachine::get_mut_ptr(), false)
// `needs_defines`: new transpiler options (a new VM, or one moved to this build).
let (vm, needs_defines): (*mut VirtualMachine, bool) = if VirtualMachine::is_loaded() {
let vm = VirtualMachine::get_mut_ptr();
// `Transpiler::init` always sets `env`, and `MacroContext` copies it.
let env = NonNull::new(env).expect("MacroContext.env is set");
// SAFETY: `vm` is the per-thread VM; uniquely accessed here. `env`
// is the build's loader and outlives every macro call of the build.
let moved = unsafe {
(*vm).serve_macro_build(env, build_options, || {
macro_vm_transform_options(build_options)
})?
};
(vm, moved)
} else {
let mut transform_options = transform_options.clone();
// Build-only flags about the output bundle. The macro module's own
// imports must still resolve.
transform_options.external = Vec::new();
transform_options.packages = None;

// JSC needs to be initialized if building from CLI
jsc::initialize(jsc::InitializeOptions::default());

let _vm = VirtualMachine::init(VirtualMachineInitOptions {
transform_options,
log: Some(NonNull::from(&mut *log)),
// No `log`: the VM outlives the build; `drain_macro_vm_log` moves its own log out.
let vm = VirtualMachine::init(VirtualMachineInitOptions {
transform_options: macro_vm_transform_options(build_options),
env_loader: NonNull::new(env),
is_main_thread: false,
..Default::default()
})?;
(_vm, true)
// SAFETY: `vm` is the freshly-allocated per-thread VM.
unsafe { (*vm).macro_build_options = Some(Arc::clone(build_options)) };
(vm, true)
};

// Covers `configure_defines` (new-VM path) and `load_macro_entry_point`
Expand All @@ -451,8 +495,8 @@ impl Macro {
let _init_guard = MacroModeGuard::new(vm);
// SAFETY: `vm` is the per-thread VM; uniquely accessed here.
unsafe { (*(*vm).event_loop()).ensure_waker() };
if is_new_vm {
// SAFETY: `vm` is the freshly-allocated per-thread VM.
if needs_defines {
// SAFETY: `vm` is the per-thread VM; uniquely accessed here.
unsafe { (*vm).transpiler.configure_defines()? };
}

Expand All @@ -477,6 +521,8 @@ impl Macro {

Ok(Macro {
vm: NonNull::new(vm),
// SAFETY: `vm` is the per-thread VM; a plain field read.
generation: unsafe { (*vm).macro_options_generation },
disabled: false,
})
}
Expand Down
42 changes: 42 additions & 0 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,11 @@ pub struct VirtualMachine {
// LAYERING: values are `MacroEntryPoint` from `bun_bundler::entry_points`
// (forward dep); stored type-erased and cast back by the consumers.
pub macro_entry_points: bun_collections::ArrayHashMap<i32, *mut c_void>,
/// Last build this macro VM served. `Some` only for a VM that `Macro::init` created.
pub macro_build_options:
Option<std::sync::Arc<bun_options_types::schema::api::TransformOptions>>,
/// Bumped when `serve_macro_build` moves the VM; a `Macro` loaded under an older value reloads.
pub macro_options_generation: u32,
pub macro_mode: bool,
/// Depth of live [`MacroModeGuard`]s on this thread. Nonzero exactly while
/// macro JS may be executing — both `MacroContext::call` and `Macro::init`
Expand Down Expand Up @@ -2057,6 +2062,41 @@ impl VirtualMachine {
promise.ok_or(crate::CrateError::JSError)
}

/// Points a macro VM at `build`: its env loader, and for another build its options (modules dropped).
pub fn serve_macro_build(
&mut self,
env: NonNull<bun_dotenv::Loader>,
build: &std::sync::Arc<bun_options_types::schema::api::TransformOptions>,
transform_options: impl FnOnce() -> bun_options_types::schema::api::TransformOptions,
) -> crate::CrateResult<bool> {
let Some(current) = &self.macro_build_options else {
return Ok(false);
};
// A `Bun.Transpiler` used inside a running macro keeps the VM as it is.
if self.macro_guard_depth != 0 {
return Ok(false);
}
// A `Worker` frees its loader when it exits; the VM may have been created from its build.
self.transpiler.env = env.as_ptr();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
Comment thread
robobun marked this conversation as resolved.
Resolved
// Held strongly, so no later build can reuse this address: identity means the same build.
if std::sync::Arc::ptr_eq(current, build) {
Comment thread
robobun marked this conversation as resolved.
Resolved
return Ok(false);
}
// Modules first: should the options fail to rebuild, the VM is the old build's, only empty.
self.macro_options_generation += 1;
for callback in self.macros.values() {
callback.unprotect();
}
self.macros.clear_retaining_capacity();
// The same clear as `bun --hot`: every module, ESM and CommonJS, goes.
self.run_with_api_lock(|| VirtualMachine::get().global().reload())
.map_err(|_| crate::CrateError::JSError)?;
self.transpiler
.reset_transform_options(transform_options())?;
self.macro_build_options = Some(std::sync::Arc::clone(build));
Ok(true)
}

pub fn is_watcher_enabled(&self) -> bool {
!self.bun_watcher.is_null()
}
Expand Down Expand Up @@ -3215,6 +3255,7 @@ impl VirtualMachine {
addr_of_mut!((*vm).resolved_path_dups).write(Vec::new());
addr_of_mut!((*vm).macros).write(Default::default());
addr_of_mut!((*vm).macro_entry_points).write(Default::default());
addr_of_mut!((*vm).macro_build_options).write(None);
addr_of_mut!((*vm).auto_killer).write(Default::default());
addr_of_mut!((*vm).root_context).write(crate::ScriptExecutionContext::root());
addr_of_mut!((*vm).graph_contexts).write(Default::default());
Expand Down Expand Up @@ -5471,6 +5512,7 @@ impl VirtualMachine {

drop(core::mem::take(&mut self.resolved_path_dups));
drop(core::mem::take(&mut self.main_resolved_path));
self.macro_build_options = None;

self.overridden_main.deinit();
self.pending_internal_promise.deinit();
Expand Down
11 changes: 5 additions & 6 deletions test/bundler/bundler_edgecase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3436,10 +3436,9 @@ describe("bundler", () => {
});
}
// The macro module is transpiled by the macro VM, not by the bundler. That
// VM has to be created from the build's transform options, or the macro
// module does not see `--define` and `--loader`. The `Bun.build()` variant
// lives in transpiler/macro-test.test.ts: the macro VM of a worker thread
// outlives the build that created it, so that test needs its own process.
// VM takes the build's transform options, or the macro module does not see
// `--define` and `--loader`. `define` picks the CLI backend; the `Bun.build()`
// variants live in transpiler/macro-test.test.ts.
itBundled("edgecase/MacroSeesBuildDefinesAndLoaders", {
files: {
"/entry.ts": /* js */ `
Expand All @@ -3457,14 +3456,15 @@ describe("bundler", () => {
`,
"/banner.dat": "hello from a text loader",
},
backend: "cli",
define: { "process.env.MODE": '"prod"' },
loader: { ".dat": "text" },
target: "bun",
run: { stdout: "prod hello from a text loader" },
});
// `--external` and `--packages=external` describe the output bundle, not the
// macro VM. A package the macro module imports has to resolve at build time.
// The API backend runs in this process, on worker VMs that earlier tests'
// builds created with other options.
itBundled("edgecase/MacroImportsPackageMarkedExternal", {
files: {
"/entry.ts": /* js */ `
Expand All @@ -3481,7 +3481,6 @@ describe("bundler", () => {
"/node_modules/foo/package.json": `{ "name": "foo", "version": "1.0.0", "main": "index.js" }`,
"/node_modules/foo/index.js": `module.exports = "foo-value";`,
},
backend: "cli",
external: ["foo"],
packages: "external",
target: "bun",
Expand Down
Loading
Loading