Compare commits

...

4 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
HK-SHAO
32a89c4334 fix(docs): code block syntax for server.tsx in SSR guide (#27417)
### What does this PR do?

TSX files may contain XML-like syntax, but TS files cannot.
2026-02-25 16:53:49 +00:00
robobun
c643e0fad8 fix(fuzzilli): prevent crash from fprintf on null FILE* in FUZZILLI_PRINT handler (#27310)
## Summary
- The `fuzzilli('FUZZILLI_PRINT', ...)` native function handler in
FuzzilliREPRL.cpp called `fdopen(103, "w")` on every invocation and
passed the result directly to `fprintf()` without a NULL check
- When running the debug-fuzz binary outside the REPRL harness (where fd
103 is not open), `fdopen()` returns NULL, and `fprintf(NULL, ...)`
causes a SIGSEGV crash
- Fix: make the `FILE*` static (so `fdopen` is called once, avoiding fd
leaks) and guard `fprintf`/`fflush` behind a NULL check

## Crash reproduction
```js
// The crash is triggered when the Fuzzilli explore framework calls
// fuzzilli('FUZZILLI_PRINT', ...) on the native fuzzilli() function
// while running outside the REPRL harness (fd 103 not open).
// Minimal reproduction:
fuzzilli('FUZZILLI_PRINT', 'hello');
```

Running the above with the debug-fuzz binary (which registers the native
`fuzzilli` function) causes SIGSEGV in `__vfprintf_internal` due to NULL
FILE*.

## Test plan
- [x] Verified crash reproduces 10/10 times with the original binary
- [x] Verified 0/10 crashes with the fixed binary
- [x] Fix is trivially correct: static FILE* + NULL guard

Co-authored-by: Alistair Smith <alistair@anthropic.com>
2026-02-25 12:52:12 +00:00
robobun
2222aa9f47 fix(bundler): write external sourcemap .map files for bun build --compile (#27396)
## Summary
- When using `bun build --compile --sourcemap=external`, the `.map`
files were embedded in the executable but never written to disk. This
fix writes them next to the compiled executable.
- With `--splitting` enabled, multiple chunks each produce their own
sourcemap. Previously all would overwrite a single `{outfile}.map`; now
each `.map` file preserves its chunk-specific name (e.g.,
`chunk-XXXXX.js.map`).
- Fixes both the JavaScript API (`Bun.build`) and CLI (`bun build`) code
paths.

## Test plan
- [x] `bun bd test test/bundler/bun-build-compile-sourcemap.test.ts` —
all 8 tests pass
- [x] New test: `compile with sourcemap: external writes .map file to
disk` — verifies a single `.map` file is written and contains valid
sourcemap JSON
- [x] New test: `compile without sourcemap does not write .map file` —
verifies no `.map` file appears without the flag
- [x] New test: `compile with splitting and external sourcemap writes
multiple .map files` — verifies each chunk gets its own uniquely-named
`.map` file on disk
- [x] Verified new tests fail with `USE_SYSTEM_BUN=1` (confirming they
test the fix, not pre-existing behavior)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-24 22:02:51 -08:00
11 changed files with 386 additions and 14 deletions

View File

@@ -33,7 +33,7 @@ const stream = await renderToReadableStream(<Component message="Hello from serve
Combining this with `Bun.serve()`, we get a simple SSR HTTP server:
```tsx server.ts icon="/icons/typescript.svg"
```tsx server.tsx icon="/icons/typescript.svg"
Bun.serve({
async fetch() {
const stream = await renderToReadableStream(<Component message="Hello from server!" />);

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

@@ -144,9 +144,15 @@ static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES functionFuzzilli(JSC::JSGlob
WTF::String output = arg1.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::jsUndefined()));
FILE* f = fdopen(REPRL_DWFD, "w");
fprintf(f, "%s\n", output.utf8().data());
fflush(f);
// Use a static FILE* to avoid repeatedly calling fdopen (which
// duplicates the descriptor and leaks) and to gracefully handle
// the case where REPRL_DWFD is not open (i.e. running outside
// the fuzzer harness).
static FILE* f = fdopen(REPRL_DWFD, "w");
if (f) {
fprintf(f, "%s\n", output.utf8().data());
fflush(f);
}
}
}

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

@@ -2179,15 +2179,67 @@ pub const BundleV2 = struct {
output_file.is_executable = true;
}
// Write external sourcemap files next to the compiled executable and
// keep them in the output array. Destroy all other non-entry-point files.
// With --splitting, there can be multiple sourcemap files (one per chunk).
var kept: usize = 0;
for (output_files.items, 0..) |*current, i| {
if (i != entry_point_index) {
if (i == entry_point_index) {
output_files.items[kept] = current.*;
kept += 1;
} else if (result == .success and current.output_kind == .sourcemap and current.value == .buffer) {
const sourcemap_bytes = current.value.buffer.bytes;
if (sourcemap_bytes.len > 0) {
// Derive the .map filename from the sourcemap's own dest_path,
// placed in the same directory as the compiled executable.
const map_basename = if (current.dest_path.len > 0)
bun.path.basename(current.dest_path)
else
bun.path.basename(bun.handleOom(std.fmt.allocPrint(bun.default_allocator, "{s}.map", .{full_outfile_path})));
const sourcemap_full_path = if (dirname.len == 0 or strings.eqlComptime(dirname, "."))
bun.handleOom(bun.default_allocator.dupe(u8, map_basename))
else
bun.handleOom(std.fmt.allocPrint(bun.default_allocator, "{s}{c}{s}", .{ dirname, std.fs.path.sep, map_basename }));
// Write the sourcemap file to disk next to the executable
var pathbuf: bun.PathBuffer = undefined;
const write_path = if (Environment.isWindows) sourcemap_full_path else map_basename;
switch (bun.jsc.Node.fs.NodeFS.writeFileWithPathBuffer(
&pathbuf,
.{
.data = .{ .buffer = .{
.buffer = .{
.ptr = @constCast(sourcemap_bytes.ptr),
.len = @as(u32, @truncate(sourcemap_bytes.len)),
.byte_len = @as(u32, @truncate(sourcemap_bytes.len)),
},
} },
.encoding = .buffer,
.dirfd = .fromStdDir(root_dir),
.file = .{ .path = .{
.string = bun.PathString.init(write_path),
} },
},
)) {
.err => |err| {
bun.Output.err(err, "failed to write sourcemap file '{s}'", .{write_path});
current.deinit();
},
.result => {
current.dest_path = sourcemap_full_path;
output_files.items[kept] = current.*;
kept += 1;
},
}
} else {
current.deinit();
}
} else {
current.deinit();
}
}
const entry_point_output_file = output_files.swapRemove(entry_point_index);
output_files.items.len = 1;
output_files.items[0] = entry_point_output_file;
output_files.items.len = kept;
return result;
}

View File

@@ -546,6 +546,57 @@ pub const BuildCommand = struct {
Global.exit(1);
}
// Write external sourcemap files next to the compiled executable.
// With --splitting, there can be multiple .map files (one per chunk).
if (this_transpiler.options.source_map == .external) {
for (output_files) |f| {
if (f.output_kind == .sourcemap and f.value == .buffer) {
const sourcemap_bytes = f.value.buffer.bytes;
if (sourcemap_bytes.len == 0) continue;
// Use the sourcemap's own dest_path basename if available,
// otherwise fall back to {outfile}.map
const map_basename = if (f.dest_path.len > 0)
bun.path.basename(f.dest_path)
else brk: {
const exe_base = bun.path.basename(outfile);
break :brk if (compile_target.os == .windows and !strings.hasSuffixComptime(exe_base, ".exe"))
try std.fmt.allocPrint(allocator, "{s}.exe.map", .{exe_base})
else
try std.fmt.allocPrint(allocator, "{s}.map", .{exe_base});
};
// root_dir already points to the outfile's parent directory,
// so use map_basename (not a path with directory components)
// to avoid writing to a doubled directory path.
var pathbuf: bun.PathBuffer = undefined;
switch (bun.jsc.Node.fs.NodeFS.writeFileWithPathBuffer(
&pathbuf,
.{
.data = .{ .buffer = .{
.buffer = .{
.ptr = @constCast(sourcemap_bytes.ptr),
.len = @as(u32, @truncate(sourcemap_bytes.len)),
.byte_len = @as(u32, @truncate(sourcemap_bytes.len)),
},
} },
.encoding = .buffer,
.dirfd = .fromStdDir(root_dir),
.file = .{ .path = .{
.string = bun.PathString.init(map_basename),
} },
},
)) {
.err => |err| {
Output.err(err, "failed to write sourcemap file '{s}'", .{map_basename});
had_err = true;
},
.result => {},
}
}
}
}
const compiled_elapsed = @divTrunc(@as(i64, @truncate(std.time.nanoTimestamp() - bundled_end)), @as(i64, std.time.ns_per_ms));
const compiled_elapsed_digit_count: isize = switch (compiled_elapsed) {
0...9 => 3,

View File

@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, tempDir } from "harness";
import { bunEnv, bunExe, tempDir } from "harness";
import { join } from "path";
describe("Bun.build compile with sourcemap", () => {
@@ -26,9 +26,9 @@ main();`,
});
expect(result.success).toBe(true);
expect(result.outputs.length).toBe(1);
const executablePath = result.outputs[0].path;
const executableOutput = result.outputs.find((o: any) => o.kind === "entry-point")!;
const executablePath = executableOutput.path;
expect(await Bun.file(executablePath).exists()).toBe(true);
// Run the compiled executable and capture the error
@@ -94,6 +94,167 @@ main();`,
expect(exitCode).not.toBe(0);
});
test("compile with sourcemap: external writes .map file to disk", async () => {
using dir = tempDir("build-compile-sourcemap-external-file", helperFiles);
const result = await Bun.build({
entrypoints: [join(String(dir), "app.js")],
compile: true,
sourcemap: "external",
});
expect(result.success).toBe(true);
const executableOutput = result.outputs.find((o: any) => o.kind === "entry-point")!;
const executablePath = executableOutput.path;
expect(await Bun.file(executablePath).exists()).toBe(true);
// The sourcemap output should appear in build result outputs
const sourcemapOutputs = result.outputs.filter((o: any) => o.kind === "sourcemap");
expect(sourcemapOutputs.length).toBe(1);
// The .map file should exist next to the executable
const mapPath = sourcemapOutputs[0].path;
expect(mapPath).toEndWith(".map");
expect(await Bun.file(mapPath).exists()).toBe(true);
// Validate the sourcemap is valid JSON with expected fields
const mapContent = JSON.parse(await Bun.file(mapPath).text());
expect(mapContent.version).toBe(3);
expect(mapContent.sources).toBeArray();
expect(mapContent.sources.length).toBeGreaterThan(0);
expect(mapContent.mappings).toBeString();
});
test("compile without sourcemap does not write .map file", async () => {
using dir = tempDir("build-compile-no-sourcemap-file", {
"nosourcemap_entry.js": helperFiles["app.js"],
"helper.js": helperFiles["helper.js"],
});
const result = await Bun.build({
entrypoints: [join(String(dir), "nosourcemap_entry.js")],
compile: true,
});
expect(result.success).toBe(true);
const executableOutput = result.outputs.find((o: any) => o.kind === "entry-point")!;
const executablePath = executableOutput.path;
// No .map file should exist next to the executable
expect(await Bun.file(`${executablePath}.map`).exists()).toBe(false);
// No sourcemap outputs should be in the result
const sourcemapOutputs = result.outputs.filter((o: any) => o.kind === "sourcemap");
expect(sourcemapOutputs.length).toBe(0);
});
test("compile with splitting and external sourcemap writes multiple .map files", async () => {
using dir = tempDir("build-compile-sourcemap-splitting", {
"entry.js": `
const mod = await import("./lazy.js");
mod.greet();
`,
"lazy.js": `
export function greet() {
console.log("hello from lazy module");
}
`,
});
const result = await Bun.build({
entrypoints: [join(String(dir), "entry.js")],
compile: true,
splitting: true,
sourcemap: "external",
});
expect(result.success).toBe(true);
const executableOutput = result.outputs.find((o: any) => o.kind === "entry-point")!;
const executablePath = executableOutput.path;
expect(await Bun.file(executablePath).exists()).toBe(true);
// With splitting and a dynamic import, there should be at least 2 sourcemaps
// (one for the entry chunk, one for the lazy-loaded chunk)
const sourcemapOutputs = result.outputs.filter((o: any) => o.kind === "sourcemap");
expect(sourcemapOutputs.length).toBeGreaterThanOrEqual(2);
// Each sourcemap should be a valid .map file on disk
const mapPaths = new Set<string>();
for (const sm of sourcemapOutputs) {
expect(sm.path).toEndWith(".map");
expect(await Bun.file(sm.path).exists()).toBe(true);
// Each map file should have a unique path (no overwrites)
expect(mapPaths.has(sm.path)).toBe(false);
mapPaths.add(sm.path);
// Validate the sourcemap is valid JSON
const mapContent = JSON.parse(await Bun.file(sm.path).text());
expect(mapContent.version).toBe(3);
expect(mapContent.mappings).toBeString();
}
// Run the compiled executable to ensure it works
await using proc = Bun.spawn({
cmd: [executablePath],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("hello from lazy module");
expect(exitCode).toBe(0);
});
test("compile with --outfile subdir/myapp writes .map next to executable", async () => {
using dir = tempDir("build-compile-sourcemap-outfile-subdir", helperFiles);
const subdirPath = join(String(dir), "subdir");
const exeSuffix = process.platform === "win32" ? ".exe" : "";
// Use CLI: bun build --compile --outfile subdir/myapp --sourcemap=external
await using proc = Bun.spawn({
cmd: [
bunExe(),
"build",
"--compile",
join(String(dir), "app.js"),
"--outfile",
join(subdirPath, "myapp"),
"--sourcemap=external",
],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [_stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
// The executable should be at subdir/myapp (with .exe on Windows)
expect(await Bun.file(join(subdirPath, `myapp${exeSuffix}`)).exists()).toBe(true);
// The .map file should be in subdir/ (next to the executable)
const glob = new Bun.Glob("*.map");
const mapFiles = Array.from(glob.scanSync({ cwd: subdirPath }));
expect(mapFiles.length).toBe(1);
// Validate the sourcemap is valid JSON
const mapContent = JSON.parse(await Bun.file(join(subdirPath, mapFiles[0])).text());
expect(mapContent.version).toBe(3);
expect(mapContent.mappings).toBeString();
// Verify no .map was written into the doubled path subdir/subdir/
expect(await Bun.file(join(String(dir), "subdir", "subdir", "myapp.map")).exists()).toBe(false);
});
test("compile with multiple source files", async () => {
using dir = tempDir("build-compile-sourcemap-multiple-files", {
"utils.js": `export function utilError() {

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 },
});
});