Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
ae01da36c7 Add readdir and createReadStream support for standalone executables
- Implement readdir method for StandaloneModuleGraph to list directory contents
- Add standalone module graph check in readdirInner function
- Support both sync and async readdir operations in executables
- createReadStream should work automatically through existing readFile support

Fixes #22223
2025-08-29 08:39:32 +00:00
8 changed files with 353 additions and 0 deletions

BIN
debug-readdir Executable file

Binary file not shown.

BIN
debug-readdir-v2 Executable file

Binary file not shown.

47
debug-readdir-v2.ts Normal file
View File

@@ -0,0 +1,47 @@
import { readdir, readdirSync, readFileSync, existsSync } from "node:fs";
// Create a file to embed
import "./foo.txt";
console.log("Testing standalone support...");
console.log("import.meta.path:", import.meta.path);
console.log("import.meta.url:", import.meta.url);
// Test import.meta.resolve approach
try {
const resolvedPath = import.meta.require.resolve("./foo.txt");
console.log("Resolved path:", resolvedPath);
const content = await Bun.file(resolvedPath).text();
console.log("File content via Bun.file:", content.trim());
// Now try readFile on the resolved path
const contentViaReadFile = readFileSync(resolvedPath, "utf8");
console.log("File content via readFileSync:", contentViaReadFile.trim());
} catch (err) {
console.error("Error with resolved path:", err.message);
}
// Try to understand what's inside the bundle by using the standalone path
console.log("Testing readdir on bundled paths...");
const pathsToTry = [
"/$bunfs/root",
"/$bunfs/root/",
"/$bunfs/",
"/$bunfs",
];
for (const path of pathsToTry) {
try {
console.log(`Trying readdir on '${path}':`);
const files = readdirSync(path);
console.log("Files:", files);
break;
} catch (err) {
console.error(`Readdir error on '${path}':`, err.message);
}
}
process.exit(0);

47
debug-readdir.ts Normal file
View File

@@ -0,0 +1,47 @@
import { readdir, readdirSync, readFileSync, existsSync } from "node:fs";
console.log("Testing standalone support...");
// Let's test what file paths are actually available
const testPaths = [
"/$bunfs/root/debug-readdir.ts",
"/$bunfs/debug-readdir.ts",
"/$bunfs/index.ts",
"/debug-readdir.ts",
"debug-readdir.ts",
];
for (const path of testPaths) {
try {
console.log(`Checking if '${path}' exists:`, existsSync(path));
if (existsSync(path)) {
const content = readFileSync(path, "utf8");
console.log(`Content preview: ${content.substring(0, 50)}...`);
}
} catch (err) {
console.error(`Error reading '${path}':`, err.message);
}
}
// Try to understand what's inside the bundle
// Test readdir on various paths
const pathsToTry = [
"/$bunfs/",
"/$bunfs",
"/$bunfs/root/",
"/$bunfs/root",
"/",
];
for (const path of pathsToTry) {
try {
console.log(`Trying readdir on '${path}':`);
const files = readdirSync(path);
console.log("Files:", files);
break;
} catch (err) {
console.error(`Readdir error on '${path}':`, err.message);
}
}
process.exit(0);

1
foo.txt Normal file
View File

@@ -0,0 +1 @@
Hello from embedded file!

View File

@@ -67,6 +67,82 @@ pub const StandaloneModuleGraph = struct {
return file.stat();
}
/// Lists files and directories within the given standalone directory path
pub fn readdir(this: *const StandaloneModuleGraph, dir_path: []const u8, allocator: std.mem.Allocator) ?std.ArrayList([]const u8) {
if (!isBunStandaloneFilePath(dir_path)) {
return null;
}
// Ensure the directory path ends with a slash for consistent matching
var normalized_dir_path_buf: bun.PathBuffer = undefined;
const normalized_dir_path = brk: {
if (dir_path.len > 0 and dir_path[dir_path.len - 1] == '/') {
break :brk dir_path;
}
if (dir_path.len >= normalized_dir_path_buf.len - 1) {
return null; // Path too long
}
@memcpy(normalized_dir_path_buf[0..dir_path.len], dir_path);
normalized_dir_path_buf[dir_path.len] = '/';
break :brk normalized_dir_path_buf[0 .. dir_path.len + 1];
};
var entries = std.ArrayList([]const u8).init(allocator);
var seen_names = std.StringHashMap(void).init(allocator);
defer seen_names.deinit();
for (this.files.keys()) |file_path| {
// Check if this file is within the requested directory
if (!bun.strings.hasPrefix(file_path, normalized_dir_path)) {
continue;
}
const relative_path = file_path[normalized_dir_path.len..];
// Skip empty relative paths (shouldn't happen, but be safe)
if (relative_path.len == 0) {
continue;
}
// Get the first path component (either a file or subdirectory name)
const slash_index = bun.strings.indexOfChar(relative_path, '/');
const entry_name = if (slash_index) |idx| relative_path[0..idx] else relative_path;
// Only add each name once (avoids duplicate directories)
if (seen_names.contains(entry_name)) {
continue;
}
// Clone the entry name for the result
const cloned_name = allocator.dupe(u8, entry_name) catch {
// Clean up on allocation failure
for (entries.items) |item| {
allocator.free(item);
}
entries.deinit();
return null;
};
entries.append(cloned_name) catch {
allocator.free(cloned_name);
// Clean up on allocation failure
for (entries.items) |item| {
allocator.free(item);
}
entries.deinit();
return null;
};
seen_names.put(entry_name, {}) catch {
// If we can't track uniqueness, continue anyway
};
}
return entries;
}
pub fn findAssumeStandalonePath(this: *const StandaloneModuleGraph, name: []const u8) ?*File {
if (Environment.isWindows) {
var normalized_buf: bun.PathBuffer = undefined;

View File

@@ -4832,6 +4832,84 @@ pub const NodeFS = struct {
const path = args.path.sliceZ(buf);
// Check for standalone executable support first
if (bun.StandaloneModuleGraph.get()) |graph| {
if (graph.readdir(path, bun.default_allocator)) |entries_list| {
defer {
for (entries_list.items) |item| {
bun.default_allocator.free(item);
}
entries_list.deinit();
}
var entries = std.ArrayList(ExpectedType).init(bun.default_allocator);
for (entries_list.items) |entry_name| {
const result: ExpectedType = switch (ExpectedType) {
bun.jsc.Node.Dirent => blk: {
// Create a Dirent object for the entry
// We need to determine if it's a file or directory
var is_dir = false;
// Check if this entry represents a directory by seeing if there are files under it
var check_path_buf: bun.PathBuffer = undefined;
const check_path = if (path.len > 0 and path[path.len - 1] == '/')
std.fmt.bufPrint(check_path_buf[0..], "{s}{s}/", .{ path, entry_name }) catch path
else
std.fmt.bufPrint(check_path_buf[0..], "{s}/{s}/", .{ path, entry_name }) catch path;
for (graph.files.keys()) |file_path| {
if (bun.strings.hasPrefix(file_path, check_path)) {
is_dir = true;
break;
}
}
break :blk bun.jsc.Node.Dirent{
.name = bun.String.cloneUTF8(entry_name),
.path = bun.String.cloneUTF8(path),
.kind = if (is_dir) .directory else .file,
};
},
bun.String => bun.String.cloneUTF8(entry_name),
Buffer => Buffer.fromBytes(
bun.handleOom(bun.default_allocator.dupe(u8, entry_name)),
bun.default_allocator,
.Uint8Array,
),
else => @compileError("unreachable"),
};
entries.append(result) catch {
// Clean up on failure
switch (ExpectedType) {
bun.jsc.Node.Dirent => {
result.name.deref();
result.path.deref();
},
Buffer => @constCast(&result).destroy(),
bun.String => result.deref(),
else => {},
}
// Clean up existing entries
for (entries.items) |*existing| {
switch (ExpectedType) {
bun.jsc.Node.Dirent => {
existing.name.deref();
existing.path.deref();
},
Buffer => @constCast(existing).destroy(),
bun.String => existing.deref(),
else => {},
}
}
entries.deinit();
return .{ .err = Syscall.Error.fromCode(.NOMEM, .open) };
};
}
return .{ .result = @unionInit(Return.Readdir, file_type, entries.items) };
}
}
if (comptime recursive and flavor == .sync) {
var buf_to_pass: bun.PathBuffer = undefined;

View File

@@ -0,0 +1,104 @@
// Tests for issue #22223: node:fs readdir and createReadStream should work in executables
import { test, expect } from "bun:test";
import { tempDirWithFiles, bunExe } from "harness";
import * as path from "path";
test("executable should support readdir and createReadStream", async () => {
const tempDir = tempDirWithFiles("issue-22223", {
"index.ts": `
import { readdir, createReadStream, existsSync } from "node:fs";
import { promisify } from "util";
const readdirAsync = promisify(readdir);
// Test readdir sync
console.log("Testing readdir sync");
const files = readdir("/$bunfs/", (err, files) => {
if (err) {
console.error("readdir error:", err.message);
process.exit(1);
}
console.log("Files found:", files.length > 0 ? "yes" : "no");
});
// Test readdir sync
try {
const fileSync = require("fs").readdirSync("/$bunfs/");
console.log("Sync files found:", fileSync.length > 0 ? "yes" : "no");
} catch (err) {
console.error("readdirSync error:", err.message);
process.exit(1);
}
// Test createReadStream
console.log("Testing createReadStream");
try {
const stream = createReadStream("/$bunfs/index.ts");
stream.on("data", () => {
console.log("Stream data received: yes");
});
stream.on("error", (err) => {
console.error("Stream error:", err.message);
process.exit(1);
});
stream.on("end", () => {
console.log("Stream ended: yes");
});
} catch (err) {
console.error("createReadStream error:", err.message);
process.exit(1);
}
`,
});
// Build executable
const executablePath = path.join(tempDir, "test-executable");
await using build = Bun.spawn({
cmd: [bunExe(), "build", "--compile", "--outfile", executablePath, path.join(tempDir, "index.ts")],
env: process.env,
stderr: "pipe",
stdout: "pipe",
cwd: tempDir,
});
const [stderr, stdout, buildExitCode] = await Promise.all([
build.stderr.text(),
build.stdout.text(),
build.exited,
]);
if (buildExitCode !== 0) {
console.error("Build stderr:", stderr);
console.error("Build stdout:", stdout);
}
expect(buildExitCode).toBe(0);
// Make executable
await Bun.$`chmod +x ${executablePath}`;
// Run the executable
await using proc = Bun.spawn({
cmd: [executablePath],
env: process.env,
stderr: "pipe",
stdout: "pipe",
cwd: tempDir,
});
const [execStderr, execStdout, execExitCode] = await Promise.all([
proc.stderr.text(),
proc.stdout.text(),
proc.exited,
]);
if (execExitCode !== 0) {
console.error("Exec stderr:", execStderr);
console.error("Exec stdout:", execStdout);
}
expect(execExitCode).toBe(0);
expect(execStdout).toContain("Files found: yes");
expect(execStdout).toContain("Sync files found: yes");
expect(execStdout).toContain("Stream data received: yes");
expect(execStdout).toContain("Stream ended: yes");
});