Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
40 changes: 25 additions & 15 deletions src/bundler/transpiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,10 +419,13 @@ impl<'a> Transpiler<'a> {
}
}

fn _resolve_entry_point(&mut self, entry_point: &[u8]) -> crate::Result<resolver::Result> {
let top_level_dir = self.fs().top_level_dir;
fn _resolve_entry_point(
&mut self,
source_dir: &[u8],
entry_point: &[u8],
) -> crate::Result<resolver::Result> {
let first = match self.resolver.resolve_with_framework(
top_level_dir,
source_dir,
entry_point,
bun_ast::ImportKind::EntryPointBuild,
) {
Expand All @@ -446,11 +449,10 @@ impl<'a> Transpiler<'a> {
prefixed.extend_from_slice(entry_point);
// `Resolver::resolve` interns the path internally,
// so the heap buffer can drop after the call.
if let Ok(r) = self.resolver.resolve(
top_level_dir,
&prefixed,
bun_ast::ImportKind::EntryPointBuild,
) {
if let Ok(r) =
self.resolver
.resolve(source_dir, &prefixed, bun_ast::ImportKind::EntryPointBuild)
{
if !r.flags.is_external() {
return Ok(r);
}
Expand All @@ -463,7 +465,18 @@ impl<'a> Transpiler<'a> {
/// Resolve an entry-point specifier, busting the directory cache and
/// retrying once on failure before reporting the error to the log.
pub fn resolve_entry_point(&mut self, entry_point: &[u8]) -> crate::Result<resolver::Result> {
match self._resolve_entry_point(entry_point) {
let top_level_dir = self.fs().top_level_dir;
self.resolve_entry_point_from(top_level_dir, entry_point)
}

/// [`Self::resolve_entry_point`], but a relative or bare `entry_point`
/// resolves from the absolute `source_dir` instead of the cwd.
Comment thread
robobun marked this conversation as resolved.
pub fn resolve_entry_point_from(
&mut self,
source_dir: &[u8],
entry_point: &[u8],
) -> crate::Result<resolver::Result> {
match self._resolve_entry_point(source_dir, entry_point) {
Ok(r) => self.reject_unbundleable_entry_point(r, entry_point),
Err(err) => {
let mut cache_bust_buf = bun_paths::path_buffer_pool::get();
Expand All @@ -476,9 +489,7 @@ impl<'a> Transpiler<'a> {
// so compute `busted` directly instead.
let busted: bool = 'name: {
// Neither buster name below would fit `cache_bust_buf`.
if self.fs().top_level_dir.len() + entry_point.len() + 4
> bun_paths::MAX_PATH_BYTES
{
if source_dir.len() + entry_point.len() + 4 > bun_paths::MAX_PATH_BYTES {
Comment thread
robobun marked this conversation as resolved.
break 'name false;
}
if bun_paths::is_absolute(entry_point) {
Expand All @@ -502,12 +513,11 @@ impl<'a> Transpiler<'a> {

// `".."` needs no platform separator rewrite.
let parts: [&[u8]; 2] = [entry_point, b".."];
let top_level_dir = self.fs().top_level_dir;

let buster_name = bun_paths::resolve_path::join_abs_string_buf_z::<
bun_paths::platform::Auto,
>(
top_level_dir, &mut cache_bust_buf[..], &parts
source_dir, &mut cache_bust_buf[..], &parts
);
self.resolver.bust_dir_cache(
bun_paths::string_paths::without_trailing_slash_windows_path(
Expand All @@ -518,7 +528,7 @@ impl<'a> Transpiler<'a> {

// Only re-query if we previously had something cached.
if busted {
if let Ok(result) = self._resolve_entry_point(entry_point) {
if let Ok(result) = self._resolve_entry_point(source_dir, entry_point) {
return self.reject_unbundleable_entry_point(result, entry_point);
}
// ignore this error, we will print the original error
Expand Down
34 changes: 25 additions & 9 deletions src/jsc/web_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ pub struct WebWorker {
exec_argv_len: usize,
inherit_exec_argv: bool,
unresolved_specifier: Box<[u8]>,
/// The cwd when `new Worker()` ran; `unresolved_specifier` resolves from
/// it, not from wherever a later `process.chdir()` went.
Comment thread
robobun marked this conversation as resolved.
cwd: Box<[u8]>,
preloads: Vec<Box<[u8]>>,
name: bun_core::ZBox,

Expand Down Expand Up @@ -304,6 +307,10 @@ impl WebWorker {
log!("[{}] create", this_context_id);

let spec_slice = specifier_str.to_utf8();
// Copied, not borrowed: `process.chdir()` rewrites the buffer behind
// `top_level_dir` in place.
// SAFETY: `parent` is the calling thread's live VM.
let cwd: Box<[u8]> = Box::from(unsafe { (*parent).top_level_dir() });
let mut temp_log = bun_ast::Log::default();
// SAFETY: `parent` is the calling thread's live VM (BACKREF); borrows
// are scoped to each statement.
Expand Down Expand Up @@ -336,7 +343,13 @@ impl WebWorker {
// SAFETY: `parent` is the live VM on the calling (parent) thread;
// `resolve_entry_point_specifier` takes the raw pointer.
if let Some(preload) = unsafe {
resolve_entry_point_specifier(parent, utf8_slice.slice(), error_message, temp_log)
resolve_entry_point_specifier(
parent,
&cwd,
utf8_slice.slice(),
error_message,
temp_log,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
)
} {
preloads.push(preload.to_vec().into_boxed_slice());
}
Expand Down Expand Up @@ -413,6 +426,7 @@ impl WebWorker {
exec_argv_len,
inherit_exec_argv,
unresolved_specifier: spec_slice.slice().to_vec().into_boxed_slice(),
cwd,
preloads,
name: if name_str.is_empty() {
bun_core::ZBox::default()
Expand Down Expand Up @@ -802,16 +816,15 @@ impl WebWorker {
// `preload: Vec<Box<[u8]>>` — clone the boxes (cheap, ≤handful).
vm.as_mut().preload.clone_from(&self.preloads);

// Resolve the entry point on the worker thread (the parent only stored
// the raw specifier). The returned slice is BORROWED — every exit from
// spin() goes through shutdown() which is noreturn, so a `defer free`
// here would never run anyway.
// Resolve the entry point here, from the cwd `create()` captured. The
// returned slice is BORROWED; every exit below ends in `shutdown()`.
Comment thread
robobun marked this conversation as resolved.
let mut resolve_error = BunString::EMPTY;
let vm_log = vm.log_mut().unwrap();
// SAFETY: `vm_ptr` is the live worker-thread VM.
let path = match unsafe {
resolve_entry_point_specifier(
vm_ptr,
&self.cwd,
&self.unresolved_specifier,
&mut resolve_error,
vm_log,
Expand Down Expand Up @@ -1263,9 +1276,9 @@ fn on_unhandled_rejection(
}

/// Resolve a worker entry-point specifier to a path the module loader can
/// consume. The returned slice is BORROWED — it aliases `str`, the
/// standalone module graph, or the resolver's arena; the caller must NOT
/// free it.
/// consume; a relative or bare `str` resolves from `cwd`. The returned slice
/// is BORROWED — it aliases `str`, the standalone module graph, or the
/// resolver's arena; the caller must NOT free it.
Comment thread
robobun marked this conversation as resolved.
///
/// # Safety
/// `parent` must point at this thread's live `VirtualMachine`. Passed as a raw
Expand All @@ -1274,6 +1287,7 @@ fn on_unhandled_rejection(
/// the single expression.
unsafe fn resolve_entry_point_specifier<'s>(
parent: *mut VirtualMachine,
cwd: &[u8],
str: &'s [u8],
error_message: &mut BunString,
log: &mut bun_ast::Log,
Expand Down Expand Up @@ -1317,7 +1331,9 @@ unsafe fn resolve_entry_point_specifier<'s>(
let global = unsafe { (*parent).global };
// SAFETY: same as above — `parent`'s `transpiler` is mutated only on its
// owning thread (the caller's thread per fn contract).
let resolved_entry_point = match unsafe { (*parent).transpiler.resolve_entry_point(str) } {
let resolved_entry_point = match unsafe {
(*parent).transpiler.resolve_entry_point_from(cwd, str)
} {
Ok(r) => r,
Err(_) => {
// `global` valid for VM lifetime; safe ZST-handle deref (panics on null).
Expand Down
48 changes: 47 additions & 1 deletion test/js/web/workers/worker-entry-point.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ describe.concurrent("package.json imports alias as the entry point", () => {
expect(stderr).toBe("");
expect(stdout).toBe("message: hi\nclosed\n");
expect(exitCode).toBe(0);
});
// LeakSanitizer's check at exit costs 4-5 s of wall time on a debug ASAN build by itself (`bun -e 1`
// with detect_leaks=1 takes as long), which is the whole default budget.
}, 30_000);

test("an alias of a builtin fires the error event", async () => {
// `new Worker("node:fs")` fails to resolve. An alias of a builtin resolves to the builtin marked
Expand All @@ -66,3 +68,47 @@ describe.concurrent("package.json imports alias as the entry point", () => {
expect(exitCode).toBe(0);
});
});

describe.concurrent("relative specifier and preload resolve from the cwd at construction", () => {
// The worker thread resolves the specifier some time after `new Worker()` returns. A process.chdir()
// in between must not change which file the specifier (or a relative `preload`) names. Several
// workers are created back to back so that the later threads have not resolved anything yet when
// chdir runs.
describe.each(["Worker", "worker_threads.Worker"])("%s", kind => {
test("process.chdir() right after new Worker()", async () => {
const post =
kind === "Worker"
? `postMessage(where + " " + globalThis.preloaded);`
: `require("node:worker_threads").parentPort.postMessage(where + " " + globalThis.preloaded);`;
const { stdout, stderr, exitCode } = await runWorkerFixture({
"preload.js": `globalThis.preloaded = "top";`,
"worker.js": `const where = "top"; ${post}`,
"other/preload.js": `globalThis.preloaded = "other";`,
"other/worker.js": `const where = "other"; ${post}`,
"main.js": `
const WorkerClass = ${kind === "Worker" ? `globalThis.Worker` : `require("node:worker_threads").Worker`};
function start() {
const worker = new WorkerClass("./worker.js", { preload: "./preload.js" });
return new Promise(resolve => {
const done = value => { resolve(value); worker.terminate(); };
if (WorkerClass === globalThis.Worker) {
worker.onmessage = event => done(event.data);
worker.onerror = event => done("error: " + event.message);
} else {
worker.on("message", done);
worker.on("error", error => done("error: " + error.message));
}
});
}
const before = [start(), start(), start(), start()];
process.chdir("other");
const after = start();
Promise.all([...before, after]).then(results => console.log(JSON.stringify(results)));
`,
});
expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual(["top top", "top top", "top top", "top top", "other other"]);
expect(exitCode).toBe(0);
});
});
});
Loading