Skip to content

resolver: anchor a relative require.resolve paths entry at the cwd - #41491

Open
robobun wants to merge 4 commits into
mainfrom
robobun/1099f95b/require-resolve-relative-paths
Open

robobun wants to merge 4 commits into
mainfrom
robobun/1099f95b/require-resolve-relative-paths

Conversation

@robobun

@robobun robobun commented Sep 6, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • require.resolve("pkg", { paths: ["./lib"] }) throws MODULE_NOT_FOUND in Bun. Node resolves it to <cwd>/lib/node_modules/pkg/index.js. The same applies to Module._resolveFilename(..., { paths }) and createRequire(...).resolve.
  • The cause: the resolver passes each paths entry as-is to check_package_path (src/resolver/resolver.rs:1974). dir_info_cached (resolver.rs:4192) returns no DirInfo for a non-absolute path, so the entry is never searched. Node runs path.resolve(from) on each entry in Module._nodeModulePaths first.

Fix

  • In the custom_dir_paths package loop, a non-absolute entry is joined against the cwd (top_level_dir) with host-native path semantics. The joined path lives in a pooled path buffer for the duration of that one check_package_path call.
  • Absolute entries are passed through unchanged. The relative-specifier loop (./x with paths) already joins against the cwd via abs_buf_checked, so it needs no change.
  • process.chdir() updates top_level_dir, so the anchor follows the current cwd like Node's process.cwd().
  • Verified: test/js/node/module/module-resolve-filename-paths.test.js (new test, passes under node --test v26.3.0, fails on bun 1.4.3). Also test/js/node/module/node-module-module.test.js and test/js/bun/resolve/.

Background

  • custom_dir_paths is a per-call slot on the resolver. Bun__resolveSyncWithPaths (src/runtime/api/BunObject.rs) sets it from options.paths and clears it when the resolve returns. The resolver then searches only those directories, not the importer's ancestors.
  • top_level_dir on the resolver's FileSystem is the process cwd. process.chdir rewrites it.
  • Fix crash in require.resolve when options.paths contains invalid entries #31566 contains the same resolver change plus C++ changes that are already on main in another form. That PR conflicts with main in three files. This PR carries only the resolver part.
Notes

Matrix checked against node v26.3.0 from a directory with lib/node_modules/foo/index.js and lib/bar.js. Entries ./lib, lib, ../<dir>/lib, absolute, .. Specifiers foo, ./bar, ./bar.js, foo/index.js. Before the fix the six bare-name cells with a relative entry differed. After the fix all 20 cells match.

When the cwd has no package.json and the paths entry does not exist, Bun falls through to auto-install from the global cache where Node throws MODULE_NOT_FOUND. That is the existing auto-install extension and is unchanged here.

test/js/bun/resolve/load-same-js-file-a-lot.test.ts ("load the same empty JS file 2000 times") times out at 5 s in this debug container with and without this diff.


[human-review] gate passed · iteration 2 · 2 files touched

fails on main (without fix)
ASAN without fix: 1 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/node/module/module-resolve-filename-paths.test.js
bun test v1.4.3 (f42e98025)

test/js/node/module/module-resolve-filename-paths.test.js:
(pass) Module._resolveFilename respects options.paths for package resolution [56.06ms]
(pass) Module._resolveFilename respects options.paths for relative paths [11.15ms]
(pass) Module._resolveFilename with overridden function receives options.paths [17.18ms]
(pass) require.resolve respects options.paths for package resolution [13.80ms]
(pass) require.resolve with relative path and options.paths (Next.js use case) [10.92ms]
(pass) Module._resolveFilename throws ERR_INVALID_ARG_TYPE if options.paths is not an array [11.23ms]
213 | 
214 |   try {
215 |     process.chdir(dir);
216 |     const expected = resolve(dir, "lib/node_modules/relative-paths-pkg/index.js");
217 | 
218 |     expect(require.resolve("relative-paths-pkg", { paths: ["./lib"] })).toBe(expected);
                 ^
ResolveMessage: Cannot find module 'relative-paths-pkg'
Require stack:
- /workspace/bun/test/js/node/module/module-re
... (truncated)

release without fix: 1 FAILED
bun test v1.4.3-canary.1 (f42e98025)

test/js/node/module/module-resolve-filename-paths.test.js:
(pass) Module._resolveFilename respects options.paths for package resolution [1.58ms]
(pass) Module._resolveFilename respects options.paths for relative paths [0.40ms]
(pass) Module._resolveFilename with overridden function receives options.paths [0.74ms]
(pass) require.resolve respects options.paths for package resolution [0.62ms]
(pass) require.resolve with relative path and options.paths (Next.js use case) [0.64ms]
(pass) Module._resolveFilename throws ERR_INVALID_ARG_TYPE if options.paths is not an array [0.18ms]
213 | 
214 |   try {
215 |     process.chdir(dir);
216 |     const expected = resolve(dir, "lib/node_modules/relative-paths-pkg/index.js");
217 | 
218 |     expect(require.resolve("relative-paths-pkg", { paths: ["./lib"] })).toBe(expected);
                 ^
ResolveMessage: Cannot find module 'relative-paths-pkg'
Require stack:
- /workspace/bun/test/js/node/module/module-resolve-filename-paths.test.js
      at <anonymous> (/workspace/bun/test/js/node/module/module-resolve-filename-paths.test.js:218:12)
(fail) require.resolve anchors a relative options.paths
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/node/module/module-resolve-filename-paths.test.js
bun test v1.4.3 (f42e98025)

test/js/node/module/module-resolve-filename-paths.test.js:
(pass) Module._resolveFilename respects options.paths for package resolution [364.29ms]
(pass) Module._resolveFilename respects options.paths for relative paths [6.03ms]
(pass) Module._resolveFilename with overridden function receives options.paths [9.85ms]
(pass) require.resolve respects options.paths for package resolution [8.29ms]
(pass) require.resolve with relative path and options.paths (Next.js use case) [6.97ms]
(pass) Module._resolveFilename throws ERR_INVALID_ARG_TYPE if options.paths is not an array [8.21ms]
(pass) require.resolve anchors a relative options.paths entry at the cwd [13.81ms]

 7 pass
 0 fail
 14 expect() calls
Ran 7 tests across 1 file. [2.46s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     403d5a9647
  features     baseline

23 deps, 131 codegen, 1172 objects in 893ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1244] gen ErrorCode+*.h
[2/1244] install /workspace/bun
bun install v1.4.3-canary.1 (f42e98025)

Checked 22 installs across 61 packages (no changes) [15.00ms]
[3/1244] gen bindgenv2
[4/1244] fetch tinycc
[tinycc] up to date
[5/1243] install /workspace/bun/packages/bun-error
bun install v1.4.3-canary.1 (f42e98025)

Checked 1 install across 2 packages (no changes) [4.00ms]
[6/1243] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[7/1216] fetch zlib
[zlib] up to date
[8/1216] gen .bind.ts → GeneratedBindings.cpp
[9/1216] gen ProcessBindingConstants.lut.h
Generating /workspace/bun/build/release/codegen/ProcessBindingConstants.lut.h from /workspace/bun/src/jsc/bindings/ProcessBindingConstants.cpp
[10/1216] install /workspace/bun/src/node-fallbacks
bun install v1.4.3-canary.1 (f42e98025)

Checked 111 installs across 104 packages (no changes) [32.00ms]
[
... (truncated)
diff hotspot
src/resolver/resolver.rs                           | 20 ++++++++----
 .../module/module-resolve-filename-paths.test.js   | 37 ++++++++++++++++++++++
 2 files changed, 51 insertions(+), 6 deletions(-)

gate history · 3 passed · 0 rejected · iteration 2

evidence per changed file
file                                                      reads  edits  tests
src/resolver/resolver.rs                                      7      4      6
…st/js/node/module/module-resolve-filename-paths.test.js      1      0      6

root cause · written by the author bot

When require.resolve(id, { paths: [...] }) or Module._resolveFilename received a relative paths entry, the resolver passed it straight to dir_info_cached, which cannot locate a non-absolute directory, so every lookup through that entry failed with MODULE_NOT_FOUND even when the package existed. The fix makes the custom_dir_paths loop in resolver.rs check bun_paths::is_absolute() and, for relative entries, join them against the current working directory via the existing FileSystem::abs_buf_checked helper into a pooled buffer before calling check_package_path, leaving abso…

Node resolves each options.paths entry with path.resolve() before it
searches node_modules under it. Bun passed the raw entry to the
resolver, which has no DirInfo for a non-absolute directory, so
require.resolve("pkg", { paths: ["./lib"] }) threw MODULE_NOT_FOUND
while Node found <cwd>/lib/node_modules/pkg.
@coderabbitai

coderabbitai Bot commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

On-demand reviews are free for the next 14 days. After that, they cost $0.25 per reviewed file.

Or wait 4 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 6a7012d2-3df0-43bd-8b0a-3e4babfff9e9

📥 Commits

Reviewing files that changed from the base of the PR and between 09927e3 and 403d5a9.

📒 Files selected for processing (2)
  • src/resolver/resolver.rs
  • test/js/node/module/module-resolve-filename-paths.test.js

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Sep 6, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: reproduced against node v26.3.0 with require.resolve("foo", { paths: ["./lib"] }) from a directory that has lib/node_modules/foo/index.js. Node returns the file, bun 1.4.3 throws MODULE_NOT_FOUND. The fix is one loop in src/resolver/resolver.rs. The test is in test/js/node/module/module-resolve-filename-paths.test.js.

CI: the diff is green on every lane. The only red test on the second run is test/napi/node-napi-tests/test/js-native-api/test_object/do.test.ts (a worker crash in the parallel batch on debian 13 x64), which also fails on main. The other failures passed on retry. Ready for review.

@robobun

robobun commented Sep 6, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 2:24 AM PT - Sep 6th, 2026

❌ @robobun, your commit 403d5a9 has 2 failures in Build #110855 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 41491

That installs a local version of the PR into your bun-41491 executable, so you can run:

bun-41491 --bun

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment thread src/resolver/resolver.rs Outdated
Comment thread src/resolver/resolver.rs Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — the follow-up commits addressed the earlier note by switching to the shared fs_ref().abs_buf_checked() helper, so both custom_dir_paths loops now normalize relative entries with the same platform::Loose semantics.

What was reviewed: the cwd-join in resolver.rs:1970-1993 — pooled buffer returns on Drop, None overflow skips the entry rather than panicking, absolute entries pass through unchanged; confirmed abs_buf_checked anchors on top_level_dir (updated by process.chdir); the new test restores process.cwd() in finally, asserts the exact MODULE_NOT_FOUND code, and stays serial (mutates cwd).

Extended reasoning...

Overview

The PR fixes require.resolve(id, { paths: [...] }) and Module._resolveFilename when a paths entry is relative. In src/resolver/resolver.rs, the custom_dir_paths package loop now checks bun_paths::is_absolute() on each entry and, for relative ones, joins against the cwd via self.fs_ref().abs_buf_checked() into a pooled bun_paths::path_buffer_pool buffer before calling check_package_path. Absolute entries are unchanged. A new test in test/js/node/module/module-resolve-filename-paths.test.js covers ./lib, lib, a fake parent module in an unrelated directory, and the MODULE_NOT_FOUND case for a non-existent relative entry.

Since the prior review, commits 9c59071 and 8012e2f replaced the hand-rolled join_abs_string_buf_checked::<platform::Auto> with the existing FileSystem::abs_buf_checked helper (platform::Loose, src/resolver/lib.rs:359), matching the sibling relative-specifier loop, and shortened the comment to a one-line why-note.

Security risks

None identified. This is a module-resolution path change scoped to the case where options.paths is explicitly provided and an entry is relative. Previously such entries were passed as-is to dir_info_cached, which returned nothing, so resolution always failed — this change moves from "never works" to "cwd-anchored like Node". No new filesystem writes, no untrusted-length arithmetic; overflow from abs_buf_checked yields None → continue, not a panic.

Level of scrutiny

Moderate. The resolver is hot-path code, but the change is gated behind custom_dir_paths.is_some() (already marked cold()) and only alters behavior for non-absolute entries that previously failed unconditionally. The implementation reuses the exact helper the neighboring relative-path loop already uses, so cross-platform separator/drive handling is consistent. The pooled buffer is scoped to one loop iteration and released via Drop. The PR gate evidence shows the new test fails on main and passes on the branch under both ASAN-debug and release.

Other factors

No CODEOWNERS entry covers src/resolver/ or the test file. The test follows the file's local createTempDir convention, restores process.cwd() in finally, asserts the specific error .code, and does not use test.concurrent (correct — it mutates process-global cwd). The earlier inline thread's concern is fully addressed by the current diff.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants