Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node compat: Add path member to fs.Dirent #7649

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from 9 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
5 changes: 5 additions & 0 deletions packages/bun-types/fs.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ declare module "fs" {
* @since v0.0.67
*/
name: string;
/**
* The base path that this `fs.Dirent` object refers to.
* @since v??????
Copy link
Member

@paperclover paperclover Jan 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since ???? should be removed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this entire file gets deleted to resolve the merge conflict -- if I'm understanding things correctly, type definitions are now in https://github.com/DefinitelyTyped/DefinitelyTyped per #7670.

*/
path: string;
}

/**
Expand Down
4 changes: 4 additions & 0 deletions src/bun.js/node/node.classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,10 @@ export default [
getter: "getName",
cache: true,
},
path: {
getter: "getPath",
cache: true,
},
},
}),
define({
Expand Down
55 changes: 47 additions & 8 deletions src/bun.js/node/node_fs.zig
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ pub const AsyncReaddirRecursiveTask = struct {
/// All the subtasks will use this fd to open files
root_fd: FileDescriptor = bun.invalid_fd,

/// This isued when joining the file paths for error messages
/// This is used when joining the file paths for error messages
root_path: PathString = PathString.empty,

pending_err: ?Syscall.Error = null,
Expand All @@ -363,6 +363,7 @@ pub const AsyncReaddirRecursiveTask = struct {
.with_file_types => |*res| {
for (res.items) |item| {
item.name.deref();
item.path.deref();
}
res.clearAndFree();
},
Expand Down Expand Up @@ -400,7 +401,8 @@ pub const AsyncReaddirRecursiveTask = struct {
bun.default_allocator.destroy(this);
}
var buf: [bun.MAX_PATH_BYTES]u8 = undefined;
this.readdir_task.performWork(this.basename.sliceAssumeZ(), &buf, false);
var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
this.readdir_task.performWork(this.basename.sliceAssumeZ(), &buf, &path_buf, false);
}
};

Expand Down Expand Up @@ -451,7 +453,13 @@ pub const AsyncReaddirRecursiveTask = struct {
return task.promise.value();
}

pub fn performWork(this: *AsyncReaddirRecursiveTask, basename: [:0]const u8, buf: *[bun.MAX_PATH_BYTES]u8, comptime is_root: bool) void {
pub fn performWork(
this: *AsyncReaddirRecursiveTask,
basename: [:0]const u8,
buf: *[bun.MAX_PATH_BYTES]u8,
path_buf: *[bun.MAX_PATH_BYTES]u8,
comptime is_root: bool
) void {
switch (this.args.tag()) {
inline else => |tag| {
const ResultType = comptime switch (tag) {
Expand All @@ -468,6 +476,7 @@ pub const AsyncReaddirRecursiveTask = struct {

switch (NodeFS.readdirWithEntriesRecursiveAsync(
buf,
path_buf,
this.args,
this,
basename,
Expand All @@ -479,7 +488,10 @@ pub const AsyncReaddirRecursiveTask = struct {
for (entries.items) |*item| {
switch (comptime ResultType) {
bun.String => item.deref(),
Dirent => item.name.deref(),
Dirent => {
item.name.deref();
item.path.deref();
},
Buffer => bun.default_allocator.free(item.buffer.byteSlice()),
else => unreachable,
}
Expand Down Expand Up @@ -509,7 +521,8 @@ pub const AsyncReaddirRecursiveTask = struct {
fn workPoolCallback(task: *JSC.WorkPoolTask) void {
var this: *AsyncReaddirRecursiveTask = @fieldParentPtr(AsyncReaddirRecursiveTask, "task", task);
var buf: [bun.MAX_PATH_BYTES]u8 = undefined;
this.performWork(this.root_path.sliceAssumeZ(), &buf, true);
var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
this.performWork(this.root_path.sliceAssumeZ(), &buf, &path_buf, true);
}

pub fn writeResults(this: *AsyncReaddirRecursiveTask, comptime ResultType: type, result: *std.ArrayList(ResultType)) void {
Expand Down Expand Up @@ -4559,6 +4572,7 @@ pub const NodeFS = struct {
switch (comptime ExpectedType) {
Dirent => {
item.name.deref();
item.path.deref();
},
Buffer => {
item.destroy();
Expand All @@ -4583,6 +4597,7 @@ pub const NodeFS = struct {
Dirent => {
entries.append(.{
.name = bun.String.create(utf8_name),
.path = bun.String.create(args.path.slice()),
.kind = current.kind,
}) catch bun.outOfMemory();
},
Expand All @@ -4601,6 +4616,7 @@ pub const NodeFS = struct {

pub fn readdirWithEntriesRecursiveAsync(
buf: *[bun.MAX_PATH_BYTES]u8,
path_buf: *[bun.MAX_PATH_BYTES]u8,
args: Arguments.Readdir,
async_task: *AsyncReaddirRecursiveTask,
basename: [:0]const u8,
Expand Down Expand Up @@ -4675,6 +4691,15 @@ pub const NodeFS = struct {
break :brk bun.path.joinZBuf(buf, &path_parts, .auto);
};

const dirent_path_to_copy = brk: {
if (async_task.root_path.sliceAssumeZ().ptr == basename.ptr) {
break :brk args.path.slice();
}

const path_parts = [_]string{ args.path.slice(), name_to_copy };
break :brk std.fs.path.dirname(bun.path.joinZBuf(path_buf, &path_parts, .auto)) orelse args.path.slice();
};

enqueue: {
switch (current.kind) {
// a symlink might be a directory or might not be
Expand All @@ -4701,7 +4726,8 @@ pub const NodeFS = struct {
switch (comptime ExpectedType) {
Dirent => {
entries.append(.{
.name = bun.String.create(name_to_copy),
.name = bun.String.create(utf8_name),
.path = bun.String.create(dirent_path_to_copy),
.kind = current.kind,
}) catch bun.outOfMemory();
},
Expand All @@ -4720,6 +4746,7 @@ pub const NodeFS = struct {

fn readdirWithEntriesRecursiveSync(
buf: *[bun.MAX_PATH_BYTES]u8,
path_buf: *[bun.MAX_PATH_BYTES]u8,
args: Arguments.Readdir,
root_basename: [:0]const u8,
comptime ExpectedType: type,
Expand Down Expand Up @@ -4811,6 +4838,15 @@ pub const NodeFS = struct {
break :brk bun.path.joinZBuf(buf, &path_parts, .auto);
};

const dirent_path_to_copy = brk: {
if (root_basename.ptr == basename.ptr) {
break :brk args.path.slice();
}

const path_parts = [_]string{ args.path.slice(), name_to_copy };
break :brk std.fs.path.dirname(bun.path.joinZBuf(path_buf, &path_parts, .auto)) orelse args.path.slice();
};

enqueue: {
switch (current.kind) {
// a symlink might be a directory or might not be
Expand All @@ -4830,7 +4866,8 @@ pub const NodeFS = struct {
switch (comptime ExpectedType) {
Dirent => {
entries.append(.{
.name = bun.String.create(name_to_copy),
.name = bun.String.create(utf8_name),
.path = bun.String.create(dirent_path_to_copy),
.kind = current.kind,
}) catch bun.outOfMemory();
},
Expand Down Expand Up @@ -4865,14 +4902,16 @@ pub const NodeFS = struct {
var path = args.path.sliceZ(buf);
if (comptime recursive and flavor == .sync) {
var buf_to_pass: [bun.MAX_PATH_BYTES]u8 = undefined;
var path_buf_to_pass: [bun.MAX_PATH_BYTES]u8 = undefined;

var entries = std.ArrayList(ExpectedType).init(bun.default_allocator);
return switch (readdirWithEntriesRecursiveSync(&buf_to_pass, args, path, ExpectedType, &entries)) {
return switch (readdirWithEntriesRecursiveSync(&buf_to_pass, &path_buf_to_pass, args, path, ExpectedType, &entries)) {
.err => |err| {
for (entries.items) |*result| {
switch (comptime ExpectedType) {
Dirent => {
result.name.deref();
result.path.deref();
},
Buffer => {
result.destroy();
Expand Down
5 changes: 5 additions & 0 deletions src/bun.js/node/types.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1728,6 +1728,7 @@ pub const Stats = union(enum) {
/// @since v12.12.0
pub const Dirent = struct {
name: bun.String,
path: bun.String,
// not publicly exposed
kind: Kind,

Expand All @@ -1743,6 +1744,10 @@ pub const Dirent = struct {
return this.name.toJS(globalObject);
}

pub fn getPath(this: *Dirent, globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue {
return this.path.toJS(globalObject);
}

pub fn isBlockDevice(
this: *Dirent,
_: *JSC.JSGlobalObject,
Expand Down
38 changes: 38 additions & 0 deletions test/dirent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// This script tests the contents of fs.Dirent for all of the methods that return it.
// Compare running with node vs. running with Bun for compatibility.

const fs = require('fs');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add automated tests

This is not an automated test. It won't run in CI.

Have a look at files with .test.js or .test.ts for an exmaple

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I now see the readdir tests in test/js/node/fs/fs.test.ts -- is it preferred to add these tests to that same file?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that sounds like a good place for the test

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some passing tests to test/js/node/fs/fs.test.ts. Ideally I would also test relative paths (i.e. readdir('../..')) for behavior consistent with node, but I'm not sure if it's worthwhile to do so, or what a good approach would be for setting the test's working directory.


// From the working directory, create a collection of files and folders for testing.
const target = process.argv[2];

// pass arguments:
// /tmp/test
// ./test
// test
// .
// ../../test
// ..

// path is always identical to the argument except for recursive entries
// For recursed entries, a leading `./` is not included

fs.mkdirSync(target + '/subfolder/childfolder/grandchildfolder', { recursive: true });
fs.writeFileSync(target + '/file.txt', 'test');
fs.writeFileSync(target + '/subfolder/childfile.txt', 'test');
fs.writeFileSync(target + '/subfolder/childfolder/grandchildfile.txt', 'test');

let results = {};
function mapFiles(files) {
return files.map(f => ({ name: f.name, path: f.path }))
.toSorted((a, b) => a.path+'/'+a.name < b.path+'/'+b.name);
}

results['fs.readdirSync'] = mapFiles(fs.readdirSync(target, { withFileTypes: true }));
results['fs.readdirSync recursive'] = mapFiles(fs.readdirSync(target, { withFileTypes: true, recursive: true }));
fs.promises.readdir(target, { withFileTypes: true, recursive: true })
.then(files => {
results['fs.promises.readdir recursive'] = mapFiles(files);
console.log(results);
});