Skip to content
Closed
71 changes: 32 additions & 39 deletions src/bundler/ParseTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ pub struct ParseTask {
pub(crate) package_version: ast::StoreStr,
pub(crate) package_name: ast::StoreStr,
pub(crate) is_entry_point: bool,
/// The import kind of the `onResolve` answer that made this task, until `BundleV2::adopt_resolver_result` runs.
pub(crate) created_by_on_resolve: Option<ast::ImportKind>,
}

pub enum ParseTaskStage {
Expand Down Expand Up @@ -237,59 +239,48 @@ impl ParseTask {
// provenance to read-only, making the later `&mut *ctx` UB).
ctx: *mut BundleV2<'_>,
) -> ParseTask {
let (package_name, package_version) = match resolve_result.package_json {
// SAFETY: `package_json` is `Option<*const PackageJSON>`; the resolver
// arena outlives the bundle pass, so deref'ing the raw pointer here to
// borrow `name`/`version` is sound.
Some(pj) => unsafe {
let pj = &*pj;
(
ast::StoreStr::new(&pj.name[..]),
ast::StoreStr::new(&pj.version[..]),
)
},
None => (ast::StoreStr::EMPTY, ast::StoreStr::EMPTY),
};
// SAFETY: lifetime erased — `ctx` outlives the ParseTask (BACKREF);
// write provenance from the `*mut BundleV2` parameter; caller passes a
// live `&mut BundleV2` coerced to `*mut`.
let ctx_ref = unsafe { bun_ptr::ParentRef::from_raw_mut(ctx.cast::<BundleV2<'static>>()) };
let known_target = ctx_ref.get().transpiler().options.target;
ParseTask {
let mut task = ParseTask {
ctx: Some(ctx_ref),
path: resolve_result.path_pair.primary,
contents_or_fd: ContentsOrFd::Fd {
dir: resolve_result.dirname_fd,
file: resolve_result.file_fd,
},
side_effects: resolve_result.primary_side_effects_data,
// D042: resolver-side and bundler-side `jsx::Pragma` are the SAME
// nominal type (`bun_options_types::jsx::Pragma`). Preserves
// jsxImportSource/runtime/etc. from tsconfig.json.
jsx: resolve_result.jsx.clone(),
source_index,
module_type: resolve_result.module_type,
emit_decorator_metadata: resolve_result.flags.emit_decorator_metadata(),
experimental_decorators: resolve_result.flags.experimental_decorators(),
use_define_for_class_fields: resolve_result.flags.use_define_for_class_fields(),
package_version,
package_name,
known_target,
// defaults:
secondary_path_for_commonjs_interop: None,
external_free_function: ExternalFreeFunction::NONE,
loader: None,
task: ThreadPoolLib::Task {
node: ThreadPoolLib::Node::default(),
callback: task_callback,
},
io_task: ThreadPoolLib::Task {
node: ThreadPoolLib::Node::default(),
callback: io_task_callback,
..Default::default()
};
task.set_resolver_result(resolve_result);
task
}

/// What the resolver says about the file, apart from its path and its descriptors.
pub(crate) fn set_resolver_result(&mut self, resolve_result: &_resolver::Result) {
(self.package_name, self.package_version) = match resolve_result.package_json {
// SAFETY: `package_json` is `Option<*const PackageJSON>`; the resolver
// arena outlives the bundle pass, so deref'ing the raw pointer here to
// borrow `name`/`version` is sound.
Some(pj) => unsafe {
let pj = &*pj;
(
ast::StoreStr::new(&pj.name[..]),
ast::StoreStr::new(&pj.version[..]),
)
},
stage: ParseTaskStage::NeedsSourceCode,
is_entry_point: false,
}
None => (ast::StoreStr::EMPTY, ast::StoreStr::EMPTY),
};
self.side_effects = resolve_result.primary_side_effects_data;
// Carries jsxImportSource, runtime and the rest from the tsconfig.json of the file.
self.jsx = resolve_result.jsx.clone();
self.module_type = resolve_result.module_type;
self.emit_decorator_metadata = resolve_result.flags.emit_decorator_metadata();
self.experimental_decorators = resolve_result.flags.experimental_decorators();
self.use_define_for_class_fields = resolve_result.flags.use_define_for_class_fields();
}

/// Re-export of `parse_worker::get_runtime_source` as an associated fn so
Expand Down Expand Up @@ -329,6 +320,7 @@ impl Default for ParseTask {
package_version: ast::StoreStr::EMPTY,
package_name: ast::StoreStr::EMPTY,
is_entry_point: false,
created_by_on_resolve: None,
}
}
}
Expand Down Expand Up @@ -608,6 +600,7 @@ pub mod parse_worker {
package_version: ast::StoreStr::EMPTY,
package_name: ast::StoreStr::EMPTY,
is_entry_point: false,
created_by_on_resolve: None,
};
let source = Source {
// `bun_ast::Source.path` is `bun_paths::fs::Path<'static>`, distinct
Expand Down
83 changes: 80 additions & 3 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2372,7 +2372,10 @@ pub mod bv2_impl {
&import_record.source_file,
&import_record.specifier,
) {
let file_map_result = _file_map_result;
let mut file_map_result = _file_map_result;
// An in-memory file has no tsconfig.json: it gets the JSX options of the build.
// SAFETY: see `transpiler` note above.
file_map_result.jsx = unsafe { &(*transpiler).options.jsx }.clone();
Comment thread
robobun marked this conversation as resolved.
let mut path_primary = file_map_result.path_pair.primary;
// reshaped for borrowck — `get_or_put` borrows `*self` mutably via
// `self.graph`; capture the slot as `*mut u32` so subsequent `self.*` calls
Expand Down Expand Up @@ -3746,7 +3749,7 @@ pub mod bv2_impl {
self.graph.input_files.append(crate::Graph::InputFile {
source: core::mem::take(source),
loader,
side_effects: loader.side_effects(),
side_effects: resolve_result.primary_side_effects_data,
..Default::default()
})?;
// `ParseTask::init` takes `bun_ast::Index`; both Index newtypes
Expand All @@ -3760,7 +3763,10 @@ pub mod bv2_impl {
// SAFETY: arena outlives the bundle pass; reborrow `*mut` as `&mut`.
let task: &mut ParseTask = self.arena_create(task_val);
task.loader = Some(loader);
task.jsx = self.transpiler_for_target(known_target).options.jsx.clone();
task.jsx.development = self
.transpiler_for_target(known_target)
.options
.forced_jsx_development();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
task.task.node.next = core::ptr::null_mut();
task.io_task.node.next = core::ptr::null_mut();
task.known_target = known_target;
Expand Down Expand Up @@ -4601,6 +4607,72 @@ pub mod bv2_impl {
}

impl<'a> BundleV2<'a> {
/// A module that `onResolve` made takes the resolver's result if its path is byte for byte the resolver's.
pub(crate) fn adopt_resolver_result(&mut self, task: &mut ParseTask) {
let Some(kind) = task.created_by_on_resolve.take() else {
return;
};
if !task.path.is_file()
|| self
.file_map
.is_some_and(|files| files.contains(task.path.text))
{
return;
}
// The resolver remembers a directory that it did not find, and a plugin can create that directory later.
if strings::contains_char(task.path.text, 0) || !bun_sys::exists(task.path.text) {
return;
}
let transpiler = self.transpiler_for_target(task.known_target);
// The resolver logs to a scratch log: the build must not fail because this call cannot list a directory.
let mut probe_log = bun_ast::Log {
level: transpiler.resolver.log_mut().level,
..Default::default()
};
let result = {
let resolver: *mut _resolver::Resolver<'a> = &raw mut transpiler.resolver;
// SAFETY: the resolver outlives the guard, the guard drops before `probe_log`, and both uses go through `resolver`.
let _restore_log = unsafe {
_resolver::Resolver::scoped_log(resolver, NonNull::from(&mut probe_log))
};
// SAFETY: `resolver` points at the live resolver, and no other borrow of it is in use.
unsafe { &mut *resolver }.resolve(
task.path.name().dir_with_trailing_slash(),
task.path.text,
kind,
)
};
if result.is_err() {
// Nothing else asks the resolver about this path, so a directory that it may not list is not a build error.
probe_log
.msgs
.retain(|msg| !msg.data.text.starts_with(b"Cannot read directory"));
let errors = probe_log
.msgs
.iter()
.filter(|msg| msg.kind == bun_ast::Kind::Err);
probe_log.errors = u32::try_from(errors.count()).expect("int cast");
}
// The resolver parses a package.json or tsconfig.json once per process, so its errors are reported now or never.
probe_log.append_to_with_recycled(transpiler.resolver.log_mut(), true);
let Ok(result) = result else {
return;
};
Comment thread
robobun marked this conversation as resolved.
// Only then can another import make this module through the resolver, and the first one to land wins.
if result.flags.is_external()
|| result
.path_const()
.is_none_or(|resolved| resolved.text != task.path.text)
{
return;
}
let jsx_development = transpiler.options.forced_jsx_development();
task.set_resolver_result(&result);
task.jsx.development = jsx_development;
self.graph.input_files.items_side_effects_mut()[task.source_index.get() as usize] =
task.side_effects;
Comment thread
robobun marked this conversation as resolved.
}

pub(crate) fn on_load(load: &mut jsc_api::JSBundler::Load, this: &mut BundleV2) {
if load.deferred_in.take() == Some(this.graph.defer_epoch) {
// Answered while `.defer()`red and before that batch was drained (cancelled, or a plugin
Expand Down Expand Up @@ -4648,6 +4720,7 @@ pub mod bv2_impl {
// If it's a file namespace, we should run it through the parser like normal.
// The file could be on disk.
if source.path.is_file() {
this.adopt_resolver_result(load.parse_task_mut());
this.graph.pool().schedule(load.parse_task_mut());
return;
}
Expand All @@ -4668,6 +4741,8 @@ pub mod bv2_impl {
this.decrement_scan_counter();
}
jsc_api::JSBundler::LoadValue::Success(code) => {
// Not before `onLoad`: it can write files the module imports, and the resolver lists a directory once.
this.adopt_resolver_result(load.parse_task_mut());
// `code`: LoadSuccess { source_code, loader }
// When a plugin returns a file loader, we always need to populate additional_files
let should_copy_for_bundling = code.loader.should_copy_for_bundling();
Expand Down Expand Up @@ -5047,6 +5122,7 @@ pub mod bv2_impl {
known_target: resolve.import_record.original_target,
is_entry_point: resolve.import_record.kind
== ImportKind::EntryPointBuild,
created_by_on_resolve: Some(resolve.import_record.kind),
..Default::default()
};
// Arena-owned.
Expand All @@ -5057,6 +5133,7 @@ pub mod bv2_impl {
this.increment_scan_counter();

if !this.enqueue_on_load_plugin_if_needed(task) {
this.adopt_resolver_result(task);
if loader.should_copy_for_bundling() {
let additional_files: &mut bun_alloc::AstVec<
crate::AdditionalFile,
Expand Down
Loading
Loading