From b5dd396f5d11ac4c489acb419c4eb203291208d3 Mon Sep 17 00:00:00 2001 From: Mustaque Ahmed Date: Sun, 26 Oct 2025 16:02:44 +0530 Subject: [PATCH 1/4] feat: add `findPackageJSON` in bun --- src/bun.js/modules/NodeModuleModule.cpp | 56 +++++++++++++++++ src/bun.js/node/path.zig | 63 +++++++++++++++++++ .../node-module-findPackageJSON.test.ts | 52 +++++++++++++++ 3 files changed, 171 insertions(+) create mode 100644 test/js/node/module/node-module-findPackageJSON.test.ts diff --git a/src/bun.js/modules/NodeModuleModule.cpp b/src/bun.js/modules/NodeModuleModule.cpp index 6655ea9fbc3c..96d98e5f966d 100644 --- a/src/bun.js/modules/NodeModuleModule.cpp +++ b/src/bun.js/modules/NodeModuleModule.cpp @@ -33,6 +33,7 @@ JSC_DECLARE_HOST_FUNCTION(jsFunctionDebugNoop); JSC_DECLARE_HOST_FUNCTION(jsFunctionFindPath); JSC_DECLARE_HOST_FUNCTION(jsFunctionIsBuiltinModule); JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeModuleCreateRequire); +JSC_DECLARE_HOST_FUNCTION(jsFunctionFindPackageJSON); JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeModuleModuleConstructor); JSC_DECLARE_HOST_FUNCTION(jsFunctionResolveFileName); JSC_DECLARE_HOST_FUNCTION(jsFunctionResolveLookupPaths); @@ -288,6 +289,60 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeModuleCreateRequire, scope, JSValue::encode(Bun::JSCommonJSModule::createBoundRequireFunction(vm, globalObject, val))); } +extern "C" void Bun__findPackageJSON(BunString* path, BunString* result); + +JSC_DEFINE_HOST_FUNCTION(jsFunctionFindPackageJSON, + (JSC::JSGlobalObject * globalObject, + JSC::CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (callFrame->argumentCount() < 1) { + return Bun::throwError(globalObject, scope, + Bun::ErrorCode::ERR_MISSING_ARGS, + "findPackageJSON() requires at least one argument"_s); + } + + auto argument = callFrame->uncheckedArgument(0); + auto val = argument.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + // Convert file:// URL to path if needed + if (!isAbsolutePath(val)) { + WTF::URL url(val); + if (!url.isValid()) { + ERR::INVALID_ARG_VALUE(scope, globalObject, + "path"_s, argument, + "must be a file URL or absolute path"_s); + RELEASE_AND_RETURN(scope, {}); + } + if (!url.protocolIsFile()) { + ERR::INVALID_ARG_VALUE(scope, globalObject, + "path"_s, argument, + "must be a file URL"_s); + RELEASE_AND_RETURN(scope, {}); + } + val = url.fileSystemPath(); + } + + BunString input = Bun::toString(val); + BunString result; + Bun__findPackageJSON(&input, &result); + + if (result.tag == BunStringTag::Empty) { + return JSValue::encode(jsNull()); + } + + auto resultStr = result.toWTFString(); + if (!resultStr.isNull()) { + ASSERT(resultStr.impl()->refCount() == 2); + resultStr.impl()->deref(); + } + + RELEASE_AND_RETURN(scope, JSValue::encode(jsString(vm, resultStr))); +} + JSC_DEFINE_HOST_FUNCTION(jsFunctionSyncBuiltinExports, (JSGlobalObject * globalObject, CallFrame* callFrame)) @@ -834,6 +889,7 @@ builtinModules getBuiltinModulesObject PropertyCallback constants getConstantsObject PropertyCallback createRequire jsFunctionNodeModuleCreateRequire Function 1 enableCompileCache jsFunctionEnableCompileCache Function 0 +findPackageJSON jsFunctionFindPackageJSON Function 1 findSourceMap Bun__JSSourceMap__find Function 1 getCompileCacheDir jsFunctionGetCompileCacheDir Function 0 globalPaths getGlobalPathsObject PropertyCallback diff --git a/src/bun.js/node/path.zig b/src/bun.js/node/path.zig index bf8a26b857d0..deb0abda2a63 100644 --- a/src/bun.js/node/path.zig +++ b/src/bun.js/node/path.zig @@ -2969,3 +2969,66 @@ const typeBaseNameT = bun.meta.typeBaseNameT; const strings = bun.strings; const L = strings.literal; + +/// Find the nearest package.json file starting from a given path +/// Returns the absolute path to the package.json, or an empty string if not found +export fn Bun__findPackageJSON(input_path: *bun.String, result: *bun.String) void { + var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + var path_buf2: [bun.MAX_PATH_BYTES]u8 = undefined; + var slice = input_path.toUTF8(bun.default_allocator); + defer slice.deinit(); + + var current_dir = slice.slice(); + + // If the input is a file, start from its directory + // We need to make a null-terminated copy for stat + var path_buf_z: [bun.MAX_PATH_BYTES:0]u8 = undefined; + if (current_dir.len < path_buf_z.len) { + @memcpy(path_buf_z[0..current_dir.len], current_dir); + path_buf_z[current_dir.len] = 0; + const current_dir_z = path_buf_z[0..current_dir.len :0]; + + const stat_result = bun.sys.stat(current_dir_z); + if (stat_result == .result) { + const mode = stat_result.result.mode; + const S = bun.S; + // Check if it's a regular file (not a directory) + if ((mode & S.IFMT) == S.IFREG) { + current_dir = if (Environment.isWindows) + dirnameWindowsT(u8, current_dir) + else + dirnamePosixT(u8, current_dir); + } + } + } + + while (true) { + const pkg_path = if (Environment.isWindows) + joinWindowsT(u8, &.{ current_dir, "package.json" }, &path_buf, &path_buf2) + else + joinPosixT(u8, &.{ current_dir, "package.json" }, &path_buf, &path_buf2); + + var pkg_path_buf_z: [bun.MAX_PATH_BYTES:0]u8 = undefined; + @memcpy(pkg_path_buf_z[0..pkg_path.len], pkg_path); + pkg_path_buf_z[pkg_path.len] = 0; + + const pkg_path_z = pkg_path_buf_z[0..pkg_path.len :0]; + if (bun.sys.existsZ(pkg_path_z)) { + result.* = bun.String.cloneUTF8(pkg_path); + return; + } + + const parent = if (Environment.isWindows) + dirnameWindowsT(u8, current_dir) + else + dirnamePosixT(u8, current_dir); + + if (strings.eql(parent, current_dir)) { + break; + } + current_dir = parent; + } + + // Not found + result.* = bun.String.empty; +} diff --git a/test/js/node/module/node-module-findPackageJSON.test.ts b/test/js/node/module/node-module-findPackageJSON.test.ts new file mode 100644 index 000000000000..1ae44cebe8a2 --- /dev/null +++ b/test/js/node/module/node-module-findPackageJSON.test.ts @@ -0,0 +1,52 @@ +import { describe, test, expect } from "bun:test"; +import { findPackageJSON } from "node:module"; +import { pathToFileURL } from "node:url"; +import path from "node:path"; + +describe("Module.findPackageJSON", () => { + test("finds package.json from file URL", () => { + const fileUrl = pathToFileURL(__filename).href; + const result = findPackageJSON(fileUrl); + + expect(result).toBeDefined(); + expect(result).toContain("package.json"); + expect(result).toContain(path.resolve(import.meta.dir, "../../../..")); + }); + + test("finds package.json from directory path", () => { + const dirUrl = pathToFileURL(import.meta.dir).href; + const result = findPackageJSON(dirUrl); + + expect(result).toBeDefined(); + expect(result).toContain("package.json"); + }); + + test("finds package.json from nested file", () => { + const nestedPath = path.join(import.meta.dir, "../../.."); + const fileUrl = pathToFileURL(path.join(nestedPath, "some-file.js")).href; + const result = findPackageJSON(fileUrl); + + expect(result).toBeDefined(); + expect(result).toContain("package.json"); + }); + + test("returns null when no package.json found", () => { + // Use a path that's unlikely to have a package.json + const rootPath = path.parse(import.meta.dir).root; + const deepPath = path.join(rootPath, "nonexistent", "deep", "path", "file.js"); + const fileUrl = pathToFileURL(deepPath).href; + const result = findPackageJSON(fileUrl); + + // Should return null or empty when not found + expect(result === null || result === "").toBe(true); + }); + + test("works with absolute paths as file URLs", () => { + const absolutePath = path.resolve(import.meta.dir, "node-module-findPackageJSON.test.ts"); + const fileUrl = pathToFileURL(absolutePath).href; + const result = findPackageJSON(fileUrl); + + expect(result).toBeDefined(); + expect(result).toContain("package.json"); + }); +}); From 0abeef9967eee934787c42f04726b43d6b5488ff Mon Sep 17 00:00:00 2001 From: Mustaque Ahmed Date: Sun, 26 Oct 2025 16:29:42 +0530 Subject: [PATCH 2/4] refactor: address review comments --- src/bun.js/node/path.zig | 30 +++++++++++-- .../node-module-findPackageJSON.test.ts | 44 ++++++++++++------- 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/src/bun.js/node/path.zig b/src/bun.js/node/path.zig index deb0abda2a63..ea669eab6105 100644 --- a/src/bun.js/node/path.zig +++ b/src/bun.js/node/path.zig @@ -2999,6 +2999,20 @@ export fn Bun__findPackageJSON(input_path: *bun.String, result: *bun.String) voi else dirnamePosixT(u8, current_dir); } + } else { + // Heuristic: if path doesn't exist and doesn't end with a separator, + // treat it as a file path and start from its dirname. + if (current_dir.len > 0) { + const last = current_dir[current_dir.len - 1]; + if ((Environment.isWindows and (last != CHAR_BACKWARD_SLASH and last != CHAR_FORWARD_SLASH)) or + (!Environment.isWindows and last != CHAR_FORWARD_SLASH)) + { + current_dir = if (Environment.isWindows) + dirnameWindowsT(u8, current_dir) + else + dirnamePosixT(u8, current_dir); + } + } } } @@ -3009,13 +3023,23 @@ export fn Bun__findPackageJSON(input_path: *bun.String, result: *bun.String) voi joinPosixT(u8, &.{ current_dir, "package.json" }, &path_buf, &path_buf2); var pkg_path_buf_z: [bun.MAX_PATH_BYTES:0]u8 = undefined; + if (pkg_path.len >= pkg_path_buf_z.len) { + // Path too long; treat as not found to avoid overflow + result.* = bun.String.empty; + return; + } @memcpy(pkg_path_buf_z[0..pkg_path.len], pkg_path); pkg_path_buf_z[pkg_path.len] = 0; const pkg_path_z = pkg_path_buf_z[0..pkg_path.len :0]; - if (bun.sys.existsZ(pkg_path_z)) { - result.* = bun.String.cloneUTF8(pkg_path); - return; + const st = bun.sys.stat(pkg_path_z); + if (st == .result) { + const mode = st.result.mode; + const S = bun.S; + if ((mode & S.IFMT) == S.IFREG) { + result.* = bun.String.cloneUTF8(pkg_path); + return; + } } const parent = if (Environment.isWindows) diff --git a/test/js/node/module/node-module-findPackageJSON.test.ts b/test/js/node/module/node-module-findPackageJSON.test.ts index 1ae44cebe8a2..987cbd50964c 100644 --- a/test/js/node/module/node-module-findPackageJSON.test.ts +++ b/test/js/node/module/node-module-findPackageJSON.test.ts @@ -3,50 +3,60 @@ import { findPackageJSON } from "node:module"; import { pathToFileURL } from "node:url"; import path from "node:path"; -describe("Module.findPackageJSON", () => { - test("finds package.json from file URL", () => { +describe.concurrent("Module.findPackageJSON", () => { + test.concurrent("finds package.json from file URL", () => { const fileUrl = pathToFileURL(__filename).href; const result = findPackageJSON(fileUrl); - expect(result).toBeDefined(); - expect(result).toContain("package.json"); - expect(result).toContain(path.resolve(import.meta.dir, "../../../..")); + expect(typeof result).toBe("string"); + expect(path.basename(result!)).toBe("package.json"); + expect(path.isAbsolute(result!)).toBe(true); }); - test("finds package.json from directory path", () => { + test.concurrent("finds package.json from directory path", () => { const dirUrl = pathToFileURL(import.meta.dir).href; const result = findPackageJSON(dirUrl); - expect(result).toBeDefined(); - expect(result).toContain("package.json"); + expect(typeof result).toBe("string"); + expect(path.basename(result!)).toBe("package.json"); + expect(path.isAbsolute(result!)).toBe(true); }); - test("finds package.json from nested file", () => { + test.concurrent("finds package.json from nested file", () => { const nestedPath = path.join(import.meta.dir, "../../.."); const fileUrl = pathToFileURL(path.join(nestedPath, "some-file.js")).href; const result = findPackageJSON(fileUrl); - expect(result).toBeDefined(); - expect(result).toContain("package.json"); + expect(typeof result).toBe("string"); + expect(path.basename(result!)).toBe("package.json"); + expect(path.isAbsolute(result!)).toBe(true); }); - test("returns null when no package.json found", () => { + test.concurrent("returns null when no package.json found", () => { // Use a path that's unlikely to have a package.json const rootPath = path.parse(import.meta.dir).root; const deepPath = path.join(rootPath, "nonexistent", "deep", "path", "file.js"); const fileUrl = pathToFileURL(deepPath).href; const result = findPackageJSON(fileUrl); - // Should return null or empty when not found - expect(result === null || result === "").toBe(true); + // Should return null when not found + expect(result).toBeNull(); }); - test("works with absolute paths as file URLs", () => { + test.concurrent("works with absolute paths as file URLs", () => { const absolutePath = path.resolve(import.meta.dir, "node-module-findPackageJSON.test.ts"); const fileUrl = pathToFileURL(absolutePath).href; const result = findPackageJSON(fileUrl); - expect(result).toBeDefined(); - expect(result).toContain("package.json"); + expect(typeof result).toBe("string"); + expect(path.basename(result!)).toBe("package.json"); + expect(path.isAbsolute(result!)).toBe(true); + }); + + test.concurrent("accepts absolute path string", () => { + const result = findPackageJSON(import.meta.dir); + expect(typeof result).toBe("string"); + expect(path.basename(result!)).toBe("package.json"); + expect(path.isAbsolute(result!)).toBe(true); }); }); From 2dc3e8e1e051eb46b16933ef0283c5ed4992de95 Mon Sep 17 00:00:00 2001 From: Mustaque Ahmed Date: Mon, 27 Oct 2025 12:23:59 +0530 Subject: [PATCH 3/4] feat: use `resolver` cache to find parent directory --- src/bun.js/modules/NodeModuleModule.cpp | 4 +-- src/bun.js/node/path.zig | 48 ++++++++++--------------- 2 files changed, 21 insertions(+), 31 deletions(-) diff --git a/src/bun.js/modules/NodeModuleModule.cpp b/src/bun.js/modules/NodeModuleModule.cpp index 96d98e5f966d..6db9901a9df0 100644 --- a/src/bun.js/modules/NodeModuleModule.cpp +++ b/src/bun.js/modules/NodeModuleModule.cpp @@ -289,7 +289,7 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeModuleCreateRequire, scope, JSValue::encode(Bun::JSCommonJSModule::createBoundRequireFunction(vm, globalObject, val))); } -extern "C" void Bun__findPackageJSON(BunString* path, BunString* result); +extern "C" void Bun__findPackageJSON(JSC::JSGlobalObject* globalObject, BunString* path, BunString* result); JSC_DEFINE_HOST_FUNCTION(jsFunctionFindPackageJSON, (JSC::JSGlobalObject * globalObject, @@ -328,7 +328,7 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionFindPackageJSON, BunString input = Bun::toString(val); BunString result; - Bun__findPackageJSON(&input, &result); + Bun__findPackageJSON(globalObject, &input, &result); if (result.tag == BunStringTag::Empty) { return JSValue::encode(jsNull()); diff --git a/src/bun.js/node/path.zig b/src/bun.js/node/path.zig index ea669eab6105..79b61422357a 100644 --- a/src/bun.js/node/path.zig +++ b/src/bun.js/node/path.zig @@ -2970,18 +2970,16 @@ const typeBaseNameT = bun.meta.typeBaseNameT; const strings = bun.strings; const L = strings.literal; -/// Find the nearest package.json file starting from a given path +/// Find the nearest package.json file starting from a given path using the resolver cache /// Returns the absolute path to the package.json, or an empty string if not found -export fn Bun__findPackageJSON(input_path: *bun.String, result: *bun.String) void { - var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; - var path_buf2: [bun.MAX_PATH_BYTES]u8 = undefined; +export fn Bun__findPackageJSON(globalObject: *jsc.JSGlobalObject, input_path: *bun.String, result: *bun.String) void { var slice = input_path.toUTF8(bun.default_allocator); defer slice.deinit(); var current_dir = slice.slice(); // If the input is a file, start from its directory - // We need to make a null-terminated copy for stat + // Check if it's a regular file by trying to get parent directory var path_buf_z: [bun.MAX_PATH_BYTES:0]u8 = undefined; if (current_dir.len < path_buf_z.len) { @memcpy(path_buf_z[0..current_dir.len], current_dir); @@ -3016,41 +3014,33 @@ export fn Bun__findPackageJSON(input_path: *bun.String, result: *bun.String) voi } } - while (true) { - const pkg_path = if (Environment.isWindows) - joinWindowsT(u8, &.{ current_dir, "package.json" }, &path_buf, &path_buf2) - else - joinPosixT(u8, &.{ current_dir, "package.json" }, &path_buf, &path_buf2); - - var pkg_path_buf_z: [bun.MAX_PATH_BYTES:0]u8 = undefined; - if (pkg_path.len >= pkg_path_buf_z.len) { - // Path too long; treat as not found to avoid overflow - result.* = bun.String.empty; - return; - } - @memcpy(pkg_path_buf_z[0..pkg_path.len], pkg_path); - pkg_path_buf_z[pkg_path.len] = 0; + // Use the resolver's DirInfo cache to find package.json + const bun_vm = globalObject.bunVM(); + const resolver = &bun_vm.transpiler.resolver; - const pkg_path_z = pkg_path_buf_z[0..pkg_path.len :0]; - const st = bun.sys.stat(pkg_path_z); - if (st == .result) { - const mode = st.result.mode; - const S = bun.S; - if ((mode & S.IFMT) == S.IFREG) { + // Walk up the directory tree using the resolver cache + var search_dir = current_dir; + while (true) { + // Try to get DirInfo from the cache + if (resolver.readDirInfo(search_dir) catch null) |dir_info| { + if (dir_info.package_json) |pkg_json| { + // Found a package.json in the cache + const pkg_path = pkg_json.source.path.text; result.* = bun.String.cloneUTF8(pkg_path); return; } } + // Move to parent directory const parent = if (Environment.isWindows) - dirnameWindowsT(u8, current_dir) + dirnameWindowsT(u8, search_dir) else - dirnamePosixT(u8, current_dir); + dirnamePosixT(u8, search_dir); - if (strings.eql(parent, current_dir)) { + if (strings.eql(parent, search_dir)) { break; } - current_dir = parent; + search_dir = parent; } // Not found From d91bd21040aba707fd75424b64cc98374b07b4b2 Mon Sep 17 00:00:00 2001 From: Mustaque Ahmed Date: Mon, 27 Oct 2025 12:33:05 +0530 Subject: [PATCH 4/4] fix: coderabbitai review comment --- src/bun.js/node/path.zig | 33 ++++++++++++++------------------- src/resolver/resolver.zig | 2 +- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/src/bun.js/node/path.zig b/src/bun.js/node/path.zig index 79b61422357a..f92bc948b69d 100644 --- a/src/bun.js/node/path.zig +++ b/src/bun.js/node/path.zig @@ -2977,11 +2977,19 @@ export fn Bun__findPackageJSON(globalObject: *jsc.JSGlobalObject, input_path: *b defer slice.deinit(); var current_dir = slice.slice(); + if (current_dir.len == 0) { + result.* = bun.String.empty; + return; + } // If the input is a file, start from its directory // Check if it's a regular file by trying to get parent directory var path_buf_z: [bun.MAX_PATH_BYTES:0]u8 = undefined; - if (current_dir.len < path_buf_z.len) { + if (current_dir.len >= path_buf_z.len) { + // Path too long to process + result.* = bun.String.empty; + return; + } else { @memcpy(path_buf_z[0..current_dir.len], current_dir); path_buf_z[current_dir.len] = 0; const current_dir_z = path_buf_z[0..current_dir.len :0]; @@ -3019,28 +3027,15 @@ export fn Bun__findPackageJSON(globalObject: *jsc.JSGlobalObject, input_path: *b const resolver = &bun_vm.transpiler.resolver; // Walk up the directory tree using the resolver cache - var search_dir = current_dir; - while (true) { - // Try to get DirInfo from the cache - if (resolver.readDirInfo(search_dir) catch null) |dir_info| { + // Use std.fs.path.dirname to get null once we reach the root + var search_dir: ?[]const u8 = current_dir; + while (search_dir) |dir| : (search_dir = std.fs.path.dirname(dir)) { + if (resolver.dirInfoCached(dir) catch null) |dir_info| { if (dir_info.package_json) |pkg_json| { - // Found a package.json in the cache - const pkg_path = pkg_json.source.path.text; - result.* = bun.String.cloneUTF8(pkg_path); + result.* = bun.String.cloneUTF8(pkg_json.source.path.text); return; } } - - // Move to parent directory - const parent = if (Environment.isWindows) - dirnameWindowsT(u8, search_dir) - else - dirnamePosixT(u8, search_dir); - - if (strings.eql(parent, search_dir)) { - break; - } - search_dir = parent; } // Not found diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index eeb483a3e3ca..b10bc9e8a8a0 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -2612,7 +2612,7 @@ pub const Resolver = struct { return PackageJSON.new(pkg); } - fn dirInfoCached(r: *ThisResolver, path: string) !?*DirInfo { + pub fn dirInfoCached(r: *ThisResolver, path: string) !?*DirInfo { return try r.dirInfoCachedMaybeLog(path, true, true); }