mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 14:22:01 +00:00
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>
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
#include <JavaScriptCore/VMTrapsInlines.h>
|
||||
#include <JavaScriptCore/CallData.h>
|
||||
#include <JavaScriptCore/JSInternalPromise.h>
|
||||
#include <JavaScriptCore/IteratorOperations.h>
|
||||
#include "JavaScriptCore/Completion.h"
|
||||
#include "JavaScriptCore/JSNativeStdFunction.h"
|
||||
#include "JSCommonJSExtensions.h"
|
||||
@@ -311,31 +312,106 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionResolveFileName,
|
||||
default: {
|
||||
JSC::JSValue moduleName = callFrame->argument(0);
|
||||
JSC::JSValue fromValue = callFrame->argument(1);
|
||||
JSC::JSValue optionsValue = callFrame->argument(3); // 4th argument is options
|
||||
auto& names = builtinNames(vm);
|
||||
|
||||
if (moduleName.isUndefinedOrNull()) {
|
||||
JSC::throwTypeError(globalObject, scope, "Module._resolveFilename expects a string"_s);
|
||||
return {};
|
||||
}
|
||||
|
||||
if (
|
||||
// fast path: it's a real CommonJS module object.
|
||||
auto* cjs = jsDynamicCast<Bun::JSCommonJSModule*>(fromValue)) {
|
||||
fromValue = cjs->filename();
|
||||
} else if
|
||||
// slow path: userland code did something weird. lets let them do that
|
||||
// weird thing.
|
||||
(fromValue.isObject()) {
|
||||
// Extract filename string from fromValue
|
||||
// Follows pattern: typeof this === "string" ? this : (this?.filename ?? this?.id ?? "")
|
||||
if (!fromValue.isString()) {
|
||||
if (
|
||||
// fast path: it's a real CommonJS module object.
|
||||
auto* cjs = jsDynamicCast<Bun::JSCommonJSModule*>(fromValue)) {
|
||||
fromValue = cjs->filename();
|
||||
} else if (fromValue.isObject()) {
|
||||
// slow path: userland code did something weird. Try filename first, then id
|
||||
auto* obj = fromValue.getObject();
|
||||
auto filenameValue = obj->getIfPropertyExists(globalObject, names.filenamePublicName());
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
auto idValue = fromValue.getObject()->getIfPropertyExists(globalObject, builtinNames(vm).filenamePublicName());
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
if (idValue) {
|
||||
if (idValue.isString()) {
|
||||
fromValue = idValue;
|
||||
if (filenameValue && filenameValue.isString()) {
|
||||
fromValue = filenameValue;
|
||||
} else {
|
||||
auto idValue = obj->getIfPropertyExists(globalObject, vm.propertyNames->id);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
if (idValue && idValue.isString()) {
|
||||
fromValue = idValue;
|
||||
} else {
|
||||
// Fallback to empty string if no valid filename or id
|
||||
fromValue = jsEmptyString(vm);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Not a string, not an object - use empty string
|
||||
fromValue = jsEmptyString(vm);
|
||||
}
|
||||
}
|
||||
|
||||
auto result = Bun__resolveSync(globalObject, JSC::JSValue::encode(moduleName), JSValue::encode(fromValue), false, true);
|
||||
// Handle options.paths if provided
|
||||
JSC::JSValue pathsValue = JSC::jsUndefined();
|
||||
if (optionsValue.isObject()) {
|
||||
pathsValue = optionsValue.getObject()->getIfPropertyExists(globalObject, JSC::Identifier::fromString(vm, "paths"_s));
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
}
|
||||
|
||||
JSC::EncodedJSValue result;
|
||||
|
||||
// If paths are provided, use Bun__resolveSyncWithPaths
|
||||
if (!pathsValue.isUndefinedOrNull()) {
|
||||
// Node.js requires options.paths to be an array
|
||||
if (!JSC::isArray(globalObject, pathsValue)) {
|
||||
Bun::throwError(globalObject, scope,
|
||||
Bun::ErrorCode::ERR_INVALID_ARG_TYPE,
|
||||
"options.paths must be an array"_s);
|
||||
return {};
|
||||
}
|
||||
|
||||
WTF::Vector<BunString> paths;
|
||||
|
||||
// Iterate through the array using forEachInIterable
|
||||
forEachInIterable(globalObject, pathsValue, [&](JSC::VM&, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue item) {
|
||||
if (scope.exception())
|
||||
return;
|
||||
|
||||
WTF::String pathStr = item.toWTFString(lexicalGlobalObject);
|
||||
if (scope.exception())
|
||||
return;
|
||||
|
||||
paths.append(Bun::toStringRef(pathStr));
|
||||
});
|
||||
|
||||
if (scope.exception()) {
|
||||
// Clean up on exception
|
||||
for (auto& path : paths) {
|
||||
path.deref();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
result = Bun__resolveSyncWithPaths(globalObject, JSC::JSValue::encode(moduleName), JSValue::encode(fromValue), false, true, paths.begin(), paths.size());
|
||||
|
||||
// Clean up BunStrings to avoid leaking
|
||||
for (auto& path : paths) {
|
||||
path.deref();
|
||||
}
|
||||
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
if (!JSC::JSValue::decode(result).isString()) {
|
||||
JSC::throwException(globalObject, scope, JSC::JSValue::decode(result));
|
||||
return {};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// No paths provided, use regular resolution
|
||||
result = Bun__resolveSync(globalObject, JSC::JSValue::encode(moduleName), JSValue::encode(fromValue), false, true);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
if (!JSC::JSValue::decode(result).isString()) {
|
||||
|
||||
Reference in New Issue
Block a user