Files
bun.sh/test/js/node/module/module-resolve-filename-paths.test.js
robobun 7197fb1f04 Fix Module._resolveFilename to pass options.paths to overridden functions (#24325)
Fixes Next.js 16 + React Compiler build failure when using Bun runtime.

## Issue
When `Module._resolveFilename` was overridden (e.g., by Next.js's
require-hook), Bun was not passing the `options` parameter (which
contains `paths`) to the override function. This caused resolution
failures when the override tried to use custom resolution paths.

Additionally, when `Module._resolveFilename` was called directly with
`options.paths`, Bun was ignoring the paths parameter and using default
resolution.

## Root Causes
1. In `ImportMetaObject.cpp`, when calling an overridden
`_resolveFilename` function, the options object with paths was not being
passed as the 4th argument.

2. In `NodeModuleModule.cpp`, `jsFunctionResolveFileName` was calling
`Bun__resolveSync` without extracting and using the `options.paths`
parameter.

## Solution
1. In `ImportMetaObject.cpp`: When `userPathList` is provided, construct
an options object with `{paths: userPathList}` and pass it as the 4th
argument to the overridden `_resolveFilename` function.

2. In `NodeModuleModule.cpp`: Extract `options.paths` from the 4th
argument and call `Bun__resolveSyncWithPaths` when paths are provided,
instead of always using `Bun__resolveSync`.

## Reproduction
Before this fix, running:
```bash
bun --bun next build --turbopack
```
on a Next.js 16 app with React Compiler enabled would fail with:
```
Cannot find module './node_modules/babel-plugin-react-compiler'
```

## Testing
- Added comprehensive tests for `Module._resolveFilename` with
`options.paths`
- Verified Next.js 16 + React Compiler + Turbopack builds successfully
with Bun
- All 5 new tests pass with the fix, 3 fail without it
- All existing tests continue to pass

## Files Changed
- `src/bun.js/bindings/ImportMetaObject.cpp` - Pass options to override
- `src/bun.js/modules/NodeModuleModule.cpp` - Handle options.paths in
_resolveFilename
- `test/js/node/module/module-resolve-filename-paths.test.js` - New test
suite

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-03 20:28:33 -08:00

202 lines
6.8 KiB
JavaScript

// Use bun:test in Bun, or node:test in Node.js
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "fs";
import Module from "module";
import { tmpdir } from "os";
import { dirname, join, resolve } from "path";
// Detect runtime and import appropriate test framework
const isBun = typeof Bun !== "undefined";
let test, expect;
if (isBun) {
({ test, expect } = await import("bun:test"));
} else {
// Node.js
const { createRequire } = await import("module");
const nodeTest = await import("node:test");
const assert = await import("node:assert/strict");
// In Node.js ES modules, require is not available, so create it
globalThis.require = createRequire(import.meta.url);
test = nodeTest.test;
// Create Bun-compatible expect from Node assert
expect = value => ({
toBe: expected => assert.strictEqual(value, expected),
toEqual: expected => assert.deepStrictEqual(value, expected),
toBeDefined: () => assert.notStrictEqual(value, undefined),
toThrow: () => {
// This is used with expect(() => ...)
assert.throws(value);
},
});
}
// Helper to create temp directory - works in both Bun and Node
function createTempDir(prefix, files) {
// Use realpathSync to resolve symlinks (handles /tmp -> /private/tmp on macOS)
const dir = realpathSync(mkdtempSync(join(tmpdir(), prefix + "-")));
for (const [filePath, content] of Object.entries(files)) {
const fullPath = join(dir, filePath);
const dirPath = dirname(fullPath);
// Create parent directories if needed
mkdirSync(dirPath, { recursive: true });
writeFileSync(fullPath, content, "utf-8");
}
return {
path: dir,
cleanup: () => rmSync(dir, { recursive: true, force: true }),
};
}
test("Module._resolveFilename respects options.paths for package resolution", () => {
const { path: dir, cleanup } = createTempDir("module-resolve-paths", {
"node_modules/test-package/package.json": JSON.stringify({ name: "test-package", main: "index.js" }),
"node_modules/test-package/index.js": "module.exports = 'test-package';",
});
try {
// Create a fake parent module in a different directory
const fakeParent = new Module("/some/other/directory/file.js");
fakeParent.filename = "/some/other/directory/file.js";
fakeParent.paths = Module._nodeModulePaths("/some/other/directory");
// Without paths option, this should fail
expect(() => {
Module._resolveFilename("test-package", fakeParent);
}).toThrow();
// With paths option, this should succeed
const resolved = Module._resolveFilename("test-package", fakeParent, false, {
paths: [dir],
});
expect(resolved).toBe(resolve(dir, "node_modules/test-package/index.js"));
} finally {
cleanup();
}
});
test("Module._resolveFilename respects options.paths for relative paths", () => {
const { path: dir, cleanup } = createTempDir("module-resolve-relative", {
"target.js": "module.exports = 'target';",
});
try {
const fakeParent = new Module("/some/other/directory/file.js");
fakeParent.filename = "/some/other/directory/file.js";
fakeParent.paths = Module._nodeModulePaths("/some/other/directory");
// With paths option pointing to dir, should resolve relative to that dir
const resolved = Module._resolveFilename("./target.js", fakeParent, false, {
paths: [dir],
});
expect(resolved).toBe(resolve(dir, "target.js"));
} finally {
cleanup();
}
});
test("Module._resolveFilename with overridden function receives options.paths", () => {
const originalResolveFilename = Module._resolveFilename;
let capturedOptions;
try {
// Override _resolveFilename to capture the options
Module._resolveFilename = function (request, parent, isMain, options) {
capturedOptions = options;
return originalResolveFilename.call(this, request, parent, isMain, options);
};
const { path: dir, cleanup } = createTempDir("module-resolve-override", {
"node_modules/test-pkg/package.json": JSON.stringify({ name: "test-pkg", main: "index.js" }),
"node_modules/test-pkg/index.js": "module.exports = 'test';",
});
try {
const fakeParent = new Module("/some/other/directory/file.js");
fakeParent.filename = "/some/other/directory/file.js";
fakeParent.paths = Module._nodeModulePaths("/some/other/directory");
const testPaths = [dir];
// Call _resolveFilename with paths option
Module._resolveFilename("test-pkg", fakeParent, false, {
paths: testPaths,
});
// Verify the override function received the options with paths
expect(capturedOptions).toBeDefined();
expect(capturedOptions.paths).toEqual(testPaths);
} finally {
cleanup();
}
} finally {
Module._resolveFilename = originalResolveFilename;
}
});
test("require.resolve respects options.paths for package resolution", () => {
const { path: dir, cleanup } = createTempDir("require-resolve-paths", {
"node_modules/resolve-test-pkg/package.json": JSON.stringify({
name: "resolve-test-pkg",
main: "index.js",
}),
"node_modules/resolve-test-pkg/index.js": "module.exports = 'resolve-test';",
});
try {
// require.resolve should work with paths option
const resolved = require.resolve("resolve-test-pkg", {
paths: [dir],
});
expect(resolved).toBe(resolve(dir, "node_modules/resolve-test-pkg/index.js"));
} finally {
cleanup();
}
});
test("require.resolve with relative path and options.paths (Next.js use case)", () => {
// This reproduces the Next.js babel-plugin-react-compiler resolution issue
const { path: dir, cleanup } = createTempDir("nextjs-style-resolve", {
"node_modules/babel-plugin-react-compiler/package.json": JSON.stringify({
name: "babel-plugin-react-compiler",
main: "dist/index.js",
}),
"node_modules/babel-plugin-react-compiler/dist/index.js": "module.exports = {};",
});
try {
// Simulate what Next.js does: resolve a relative path with explicit paths
const resolved = require.resolve("./node_modules/babel-plugin-react-compiler", {
paths: [dir],
});
expect(resolved).toBe(resolve(dir, "node_modules/babel-plugin-react-compiler/dist/index.js"));
} finally {
cleanup();
}
});
test("Module._resolveFilename throws ERR_INVALID_ARG_TYPE if options.paths is not an array", () => {
// Test with string (which is iterable but not an array)
expect(() => {
Module._resolveFilename("path", __filename, false, { paths: "/some/path" });
}).toThrow();
// Test with Set (which is iterable but not an array)
expect(() => {
Module._resolveFilename("path", __filename, false, { paths: new Set(["/some/path"]) });
}).toThrow();
// Test with object (not iterable)
expect(() => {
Module._resolveFilename("path", __filename, false, { paths: { 0: "/some/path" } });
}).toThrow();
});