Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
f70bfbe004 fix(compile): resolve Workers and import.meta.url in standalone executables
Fix two bugs with `bun build --compile` when Workers are used from
bundled dependencies:

1. Worker string paths (e.g. `new Worker("./child.js")`) now correctly
   search the $bunfs virtual filesystem. Previously, relative paths with
   a .js extension fell through the extension-mapping logic without
   being looked up in the standalone module graph.

2. `import.meta.url` is now inlined per-module with the correct $bunfs
   path during bundling for --compile. Previously, all code bundled into
   a single entry-point chunk shared the chunk's import.meta.url, so
   `new URL("./worker.js", import.meta.url)` in a dependency would
   resolve relative to the entry point instead of the dependency's
   original source location.

Closes #27464

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-26 16:14:16 +00:00
6 changed files with 104 additions and 2 deletions

View File

@@ -3899,6 +3899,20 @@ pub fn NewParser_(
}
}
/// For `--compile` mode, compute the `$bunfs` virtual filesystem path
/// corresponding to the given source filesystem path. This transforms
/// e.g. `/tmp/project/node_modules/mylib/index.js` into
/// `/$bunfs/root/node_modules/mylib/index.js`.
pub fn compileBunfsPath(p: *P, source_path: []const u8) []const u8 {
const root_dir = p.options.compile_root_dir;
const prefix = bun.StandaloneModuleGraph.base_public_path_with_default_suffix;
if (root_dir.len > 0) {
const rel = bun.path.relativePlatform(root_dir, source_path, .posix, false);
return std.fmt.allocPrint(p.allocator, "{s}{s}", .{ prefix, rel }) catch bun.outOfMemory();
}
return std.fmt.allocPrint(p.allocator, "{s}{s}", .{ prefix, source_path }) catch bun.outOfMemory();
}
pub fn keepExprSymbolName(_: *P, _value: Expr, _: string) Expr {
return _value;
// var start = p.expr_list.items.len;

View File

@@ -39,6 +39,16 @@ pub const Parser = struct {
/// able to customize what import sources are used.
framework: ?*bun.bake.Framework = null,
/// True when bundling with `bun build --compile`. Used to inline
/// import.meta.url (and related properties) per-module so that
/// bundled dependencies resolve paths relative to their original
/// source rather than the entry-point chunk.
compile: bool = false,
/// The root directory used to compute relative paths in the
/// standalone module graph. Only meaningful when `compile` is true.
compile_root_dir: []const u8 = "",
/// REPL mode: transforms code for interactive evaluation
/// - Wraps lone object literals `{...}` in parentheses
/// - Hoists variable declarations for REPL persistence

View File

@@ -410,18 +410,34 @@ pub fn AstMaybe(
}, .loc = loc };
}
// Inline import.meta properties for Bake
if (p.options.framework != null or (p.options.bundle and p.options.output_format == .cjs)) {
// Inline import.meta properties for Bake and --compile
// For --compile, we must inline these per-module so that
// bundled dependencies resolve paths relative to their
// original source file, not the entry-point chunk.
if (p.options.framework != null or (p.options.bundle and (p.options.output_format == .cjs or p.options.compile))) {
if (strings.eqlComptime(name, "dir") or strings.eqlComptime(name, "dirname")) {
if (p.options.compile) {
return p.newExpr(E.String.init(p.compileBunfsPath(p.source.path.name.dir)), name_loc);
}
// Inline import.meta.dir
return p.newExpr(E.String.init(p.source.path.name.dir), name_loc);
} else if (strings.eqlComptime(name, "file")) {
// Inline import.meta.file (filename only)
return p.newExpr(E.String.init(p.source.path.name.filename), name_loc);
} else if (strings.eqlComptime(name, "path")) {
if (p.options.compile) {
return p.newExpr(E.String.init(p.compileBunfsPath(p.source.path.text)), name_loc);
}
// Inline import.meta.path (full path)
return p.newExpr(E.String.init(p.source.path.text), name_loc);
} else if (strings.eqlComptime(name, "url")) {
if (p.options.compile) {
const bunfs_path = p.compileBunfsPath(p.source.path.text);
const bunstr = bun.String.fromBytes(bunfs_path);
defer bunstr.deref();
const url = std.fmt.allocPrint(p.allocator, "{f}", .{jsc.URL.fileURLFromString(bunstr)}) catch unreachable;
return p.newExpr(E.String.init(url), name_loc);
}
// Inline import.meta.url as file:// URL
const bunstr = bun.String.fromBytes(p.source.path.text);
defer bunstr.deref();

View File

@@ -109,6 +109,12 @@ fn resolveEntryPointSpecifier(
var base = str;
base = bun.path.joinAbsStringBuf(bun.StandaloneModuleGraph.base_public_path_with_default_suffix, &pathbuf, &.{str}, .loose);
// First, try the joined path directly (e.g. ./child.js -> /$bunfs/root/child.js)
if (graph.find(pathbuf[0..base.len])) |file| {
return file.name;
}
const extname = std.fs.path.extension(base);
// ./foo -> ./foo.js

View File

@@ -1241,6 +1241,8 @@ fn runWithSourceCode(
} else .none;
opts.framework = transpiler.options.framework;
opts.compile = transpiler.options.compile;
opts.compile_root_dir = transpiler.options.root_dir;
opts.ignore_dce_annotations = transpiler.options.ignore_dce_annotations and !source.index.isRuntime();

View File

@@ -936,4 +936,58 @@ const server = serve({
const result = await Bun.$`./app`.cwd(dir).env(bunEnv).nothrow();
expect(result.stdout.toString().trim()).toBe("IT WORKS");
});
// https://github.com/oven-sh/bun/issues/27464
// Worker with string relative path (e.g. "./worker.js") should resolve
// against the $bunfs virtual filesystem in standalone executables.
itBundled("compile/WorkerStringRelativePathJSExtension", {
backend: "cli",
compile: true,
files: {
"/entry.ts": /* js */ `
import {rmSync} from 'fs';
rmSync("./worker.js", {force: true});
console.log("Hello, world!");
new Worker("./worker.js");
`,
"/worker.ts": /* js */ `
console.log("Worker loaded!");
`.trim(),
},
entryPointsRaw: ["./entry.ts", "./worker.ts"],
outfile: "dist/out",
run: { stdout: "Hello, world!\nWorker loaded!\n", file: "dist/out", setCwd: true },
});
// https://github.com/oven-sh/bun/issues/27464
// When a dependency is bundled into an entry point, import.meta.url should
// still reflect the original source module path so that new URL("./sibling",
// import.meta.url) resolves correctly in the $bunfs virtual filesystem.
itBundled("compile/WorkerDepImportMetaURL", {
backend: "cli",
compile: true,
files: {
"/entry.ts": /* js */ `
import {rmSync} from 'fs';
import { runWorker } from "./node_modules/mylib/index.js";
// Delete files to make sure we're loading from $bunfs
rmSync("./node_modules", {recursive: true, force: true});
runWorker();
`,
"/node_modules/mylib/package.json": `{ "name": "mylib", "main": "index.js" }`,
"/node_modules/mylib/index.js": /* js */ `
export function runWorker() {
const workerURL = new URL("./worker.js", import.meta.url);
const w = new Worker(workerURL);
w.onmessage = (e) => { console.log(e.data); process.exit(0); };
w.onerror = (e) => { console.log("ERROR:", e.message); process.exit(1); };
}
`,
"/node_modules/mylib/worker.js": /* js */ `
self.postMessage("Hello from dep worker!");
`.trim(),
},
entryPointsRaw: ["./entry.ts", "./node_modules/mylib/worker.js"],
outfile: "dist/out",
run: { stdout: "Hello from dep worker!\n", file: "dist/out", setCwd: true },
});
});