diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index 2a6bb81673..c101341ca7 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -749,9 +749,9 @@ pub fn deinit(dev: *DevServer) void { dev.active_websocket_connections.deinit(allocator); }, .watcher_atomics = for (&dev.watcher_atomics.events) |*event| { - event.aligned.dirs.deinit(dev.allocator); - event.aligned.files.deinit(dev.allocator); - event.aligned.extra_files.deinit(dev.allocator); + event.dirs.deinit(dev.allocator); + event.files.deinit(dev.allocator); + event.extra_files.deinit(dev.allocator); }, .testing_batch_events = switch (dev.testing_batch_events) { .disabled => {}, @@ -6146,8 +6146,8 @@ fn markAllRouteChildrenFailed(dev: *DevServer, route_index: Route.Index) void { /// This task informs the DevServer's thread about new files to be bundled. pub const HotReloadEvent = struct { - /// Align to cache lines to eliminate contention. - const Aligned = struct { aligned: HotReloadEvent align(std.atomic.cache_line) }; + /// Align to cache lines to eliminate false sharing. + _: u0 align(std.atomic.cache_line) = 0, owner: *DevServer, /// Initialized in WatcherAtomics.watcherReleaseAndSubmitEvent @@ -6387,7 +6387,7 @@ const WatcherAtomics = struct { /// once. Memory is reused by swapping between these two. These items are /// aligned to cache lines to reduce contention, since these structures are /// carefully passed between two threads. - events: [2]HotReloadEvent.Aligned align(std.atomic.cache_line), + events: [2]HotReloadEvent align(std.atomic.cache_line), /// 0 - no watch /// 1 - has fired additional watch /// 2+ - new events available, watcher is waiting on bundler to finish @@ -6401,10 +6401,7 @@ const WatcherAtomics = struct { pub fn init(dev: *DevServer) WatcherAtomics { return .{ - .events = .{ - .{ .aligned = .initEmpty(dev) }, - .{ .aligned = .initEmpty(dev) }, - }, + .events = .{ .initEmpty(dev), .initEmpty(dev) }, .current = 0, .watcher_events_emitted = .init(0), .watcher_has_event = .{}, @@ -6417,7 +6414,7 @@ const WatcherAtomics = struct { fn watcherAcquireEvent(state: *WatcherAtomics) *HotReloadEvent { state.watcher_has_event.lock(); - var ev: *HotReloadEvent = &state.events[state.current].aligned; + var ev: *HotReloadEvent = &state.events[state.current]; switch (ev.contention_indicator.swap(1, .seq_cst)) { 0 => { // New event, initialize the timer if it is empty. @@ -6429,7 +6426,7 @@ const WatcherAtomics = struct { // DevServer stole this event. Unlikely but possible when // the user is saving very heavily (10-30 times per second) state.current +%= 1; - ev = &state.events[state.current].aligned; + ev = &state.events[state.current]; if (Environment.allow_assert) { bun.assert(ev.contention_indicator.swap(1, .seq_cst) == 0); } @@ -6503,10 +6500,10 @@ const WatcherAtomics = struct { if (state.watcher_events_emitted.swap(0, .seq_cst) >= 2) { // Cannot use `state.current` because it will contend with the watcher. // Since there are are two events, one pointer comparison suffices - const other_event = if (first_event == &state.events[0].aligned) - &state.events[1].aligned + const other_event = if (first_event == &state.events[0]) + &state.events[1] else - &state.events[0].aligned; + &state.events[0]; switch (other_event.contention_indicator.swap(1, .seq_cst)) { 0 => { diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index b83f48f276..d36ed22edc 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -901,6 +901,35 @@ export fn Bun__resolveSync(global: *JSGlobalObject, specifier: JSValue, source: return JSC.toJSHostValue(global, doResolveWithArgs(global, specifier_str, source_str, is_esm, true, is_user_require_resolve)); } +export fn Bun__resolveSyncWithPaths( + global: *JSGlobalObject, + specifier: JSValue, + source: JSValue, + is_esm: bool, + is_user_require_resolve: bool, + paths_ptr: ?[*]const bun.String, + paths_len: usize, +) JSC.JSValue { + const paths: []const bun.String = if (paths_len == 0) &.{} else paths_ptr.?[0..paths_len]; + + const specifier_str = specifier.toBunString(global) catch return .zero; + defer specifier_str.deref(); + + if (specifier_str.length() == 0) { + return global.ERR_INVALID_ARG_VALUE("The argument 'id' must be a non-empty string. Received ''", .{}).throw() catch .zero; + } + + const source_str = source.toBunString(global) catch return .zero; + defer source_str.deref(); + + const bun_vm = global.bunVM(); + bun.assert(bun_vm.transpiler.resolver.custom_dir_paths == null); + bun_vm.transpiler.resolver.custom_dir_paths = paths; + defer bun_vm.transpiler.resolver.custom_dir_paths = null; + + return JSC.toJSHostValue(global, doResolveWithArgs(global, specifier_str, source_str, is_esm, true, is_user_require_resolve)); +} + export fn Bun__resolveSyncWithStrings(global: *JSGlobalObject, specifier: *bun.String, source: *bun.String, is_esm: bool) JSC.JSValue { Output.scoped(.importMetaResolve, false)("source: {s}, specifier: {s}", .{ source.*, specifier.* }); return JSC.toJSHostValue(global, doResolveWithArgs(global, specifier.*, source.*, is_esm, true, false)); diff --git a/src/bun.js/bindings/ImportMetaObject.cpp b/src/bun.js/bindings/ImportMetaObject.cpp index 229d735dc0..aeb302ce75 100644 --- a/src/bun.js/bindings/ImportMetaObject.cpp +++ b/src/bun.js/bindings/ImportMetaObject.cpp @@ -286,6 +286,7 @@ extern "C" JSC::EncodedJSValue functionImportMeta__resolveSyncPrivate(JSC::JSGlo JSValue from = callFrame->argument(1); bool isESM = callFrame->argument(2).asBoolean(); bool isRequireDotResolve = callFrame->argument(3).isTrue(); + JSValue userPathList = callFrame->argument(4); RETURN_IF_EXCEPTION(scope, {}); @@ -339,10 +340,48 @@ extern "C" JSC::EncodedJSValue functionImportMeta__resolveSyncPrivate(JSC::JSGlo } } } + + if (!userPathList.isUndefinedOrNull()) { + if (JSArray* userPathListArray = jsDynamicCast(userPathList)) { + if (!moduleName.isString()) { + Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "id"_s, "string"_s, moduleName); + scope.release(); + return {}; + } + + JSC::EncodedJSValue result = {}; + WTF::Vector paths; + for (size_t i = 0; i < userPathListArray->length(); ++i) { + JSValue path = userPathListArray->getIndex(globalObject, i); + WTF::String pathStr = path.toWTFString(globalObject); + if (scope.exception()) goto cleanup; + paths.append(Bun::toStringRef(pathStr)); + } + + result = Bun__resolveSyncWithPaths(lexicalGlobalObject, JSC::JSValue::encode(moduleName), JSValue::encode(from), isESM, isRequireDotResolve, paths.data(), paths.size()); + if (scope.exception()) goto cleanup; + + if (!JSC::JSValue::decode(result).isString()) { + JSC::throwException(lexicalGlobalObject, scope, JSC::JSValue::decode(result)); + result = {}; + goto cleanup; + } + + cleanup: + for (auto& path : paths) { + path.deref(); + } + RELEASE_AND_RETURN(scope, result); + } else { + Bun::ERR::INVALID_ARG_VALUE(scope, globalObject, "option.paths"_s, userPathList); + scope.release(); + return {}; + } + } } if (!moduleName.isString()) { - Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "id"_s, "string"_s, moduleName); + Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, isRequireDotResolve ? "request"_s : "id"_s, "string"_s, moduleName); scope.release(); return {}; } diff --git a/src/bun.js/bindings/ImportMetaObject.h b/src/bun.js/bindings/ImportMetaObject.h index eb8779ecc9..624ec494d8 100644 --- a/src/bun.js/bindings/ImportMetaObject.h +++ b/src/bun.js/bindings/ImportMetaObject.h @@ -12,6 +12,7 @@ extern "C" JSC_DECLARE_HOST_FUNCTION(functionImportMeta__resolveSync); extern "C" JSC_DECLARE_HOST_FUNCTION(functionImportMeta__resolveSyncPrivate); extern "C" JSC::EncodedJSValue Bun__resolve(JSC::JSGlobalObject* global, JSC::EncodedJSValue specifier, JSC::EncodedJSValue from, bool is_esm); extern "C" JSC::EncodedJSValue Bun__resolveSync(JSC::JSGlobalObject* global, JSC::EncodedJSValue specifier, JSC::EncodedJSValue from, bool is_esm, bool isUserRequireResolve); +extern "C" JSC::EncodedJSValue Bun__resolveSyncWithPaths(JSC::JSGlobalObject* global, JSC::EncodedJSValue specifier, JSC::EncodedJSValue from, bool is_esm, bool isUserRequireResolve, const BunString* paths, size_t paths_len); extern "C" JSC::EncodedJSValue Bun__resolveSyncWithSource(JSC::JSGlobalObject* global, JSC::EncodedJSValue specifier, BunString* from, bool is_esm, bool isUserRequireResolve); extern "C" JSC::EncodedJSValue Bun__resolveSyncWithStrings(JSC::JSGlobalObject* global, BunString* specifier, BunString* from, bool is_esm); diff --git a/src/bun.js/bindings/JSCommonJSModule.cpp b/src/bun.js/bindings/JSCommonJSModule.cpp index d8d45de5b1..233dfac11e 100644 --- a/src/bun.js/bindings/JSCommonJSModule.cpp +++ b/src/bun.js/bindings/JSCommonJSModule.cpp @@ -29,6 +29,7 @@ * different value. In that case, it will have a stale value. */ +#include "BunString.h" #include "headers.h" #include "JavaScriptCore/CallData.h" @@ -51,7 +52,7 @@ #include "BunClientData.h" #include #include "ImportMetaObject.h" - +#include "NodeModuleModule.h" #include #include #include @@ -78,6 +79,8 @@ #include "wtf/text/StringImpl.h" #include "JSCommonJSExtensions.h" +#include "ErrorCode.h" + extern "C" bool Bun__isBunMain(JSC::JSGlobalObject* global, const BunString*); namespace Bun { @@ -278,7 +281,44 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionEvaluateCommonJSModule, (JSGlobalObject * lex JSC_DEFINE_HOST_FUNCTION(requireResolvePathsFunction, (JSGlobalObject * globalObject, CallFrame* callframe)) { - return JSValue::encode(JSC::constructEmptyArray(globalObject, nullptr, 0)); + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + JSValue request = callframe->argument(0); + + if (!request.isString()) { + Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "request"_s, "string"_s, request); + scope.release(); + return {}; + } + + auto requestStr = request.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + { + UTF8View utf8(requestStr); + auto span = utf8.span(); + if (ModuleLoader__isBuiltin(span.data(), span.size())) { + return JSC::JSValue::encode(JSC::jsNull()); + } + } + + RETURN_IF_EXCEPTION(scope, {}); + + // This function is not bound with the module object. This is because nearly + // no one uses this and it is not worth creating an extra bound function for + // every single module. Instead, we can unwrap the bound function that we + // can see through the `this`. + JSValue thisValue = callframe->thisValue(); + auto* requireResolveBound = jsDynamicCast(thisValue); + if (UNLIKELY(!requireResolveBound)) { + return JSValue::encode(constructEmptyArray(globalObject, nullptr, 0)); + } + JSValue boundThis = requireResolveBound->boundThis(); + JSString* filename = jsDynamicCast(boundThis); + if (UNLIKELY(!filename)) { + return JSValue::encode(constructEmptyArray(globalObject, nullptr, 0)); + } + RETURN_IF_EXCEPTION(scope, {}); + Bun::PathResolveModule parent = { .paths = nullptr, .filename = filename, .pathsArrayLazy = true }; + return JSValue::encode(Bun::resolveLookupPaths(globalObject, requestStr, parent)); } JSC_DEFINE_CUSTOM_GETTER(jsRequireCacheGetter, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) @@ -460,14 +500,23 @@ extern "C" JSC::EncodedJSValue Resolver__propForRequireMainPaths(JSGlobalObject* JSC_DEFINE_CUSTOM_GETTER(getterPaths, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) { + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + JSCommonJSModule* thisObject = jsDynamicCast(JSValue::decode(thisValue)); if (UNLIKELY(!thisObject)) { return JSValue::encode(jsUndefined()); } if (!thisObject->m_paths) { - JSValue paths = JSValue::decode(Resolver__propForRequireMainPaths(globalObject)); + JSValue filename = thisObject->filename(); + ASSERT(filename); + auto filenameWtfStr = filename.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + BunString filenameStr = Bun::toString(filenameWtfStr); + JSValue paths = JSValue::decode(Resolver__nodeModulePathsJSValue(filenameStr, globalObject, true)); + RETURN_IF_EXCEPTION(scope, {}); thisObject->m_paths.set(globalObject->vm(), thisObject, paths); + return JSValue::encode(paths); } return JSValue::encode(thisObject->m_paths.get()); diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index d390c024a3..c3abaf93c3 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -2535,18 +2535,13 @@ pub const VirtualMachine = struct { threadlocal var specifier_cache_resolver_buf: bun.PathBuffer = undefined; fn _resolve( + jsc_vm: *VirtualMachine, ret: *ResolveFunctionResult, specifier: string, source: string, is_esm: bool, comptime is_a_file_path: bool, ) !void { - bun.assert(VirtualMachine.isLoaded()); - // macOS threadlocal vars are very slow - // we won't change threads in this function - // so we can copy it here - var jsc_vm = VirtualMachine.get(); - if (strings.eqlComptime(std.fs.path.basename(specifier), Runtime.Runtime.Imports.alt_name)) { ret.path = Runtime.Runtime.Imports.Name; return; @@ -2712,7 +2707,7 @@ pub const VirtualMachine = struct { } var result = ResolveFunctionResult{ .path = "", .result = null }; - var jsc_vm = VirtualMachine.get(); + const jsc_vm = global.bunVM(); const specifier_utf8 = specifier.toUTF8(bun.default_allocator); defer specifier_utf8.deinit(); @@ -2755,7 +2750,7 @@ pub const VirtualMachine = struct { jsc_vm.transpiler.linker.log = old_log; jsc_vm.transpiler.resolver.log = old_log; } - _resolve(&result, specifier_utf8.slice(), normalizeSource(source_utf8.slice()), is_esm, is_a_file_path) catch |err_| { + jsc_vm._resolve(&result, specifier_utf8.slice(), normalizeSource(source_utf8.slice()), is_esm, is_a_file_path) catch |err_| { var err = err_; const msg: logger.Msg = brk: { const msgs: []logger.Msg = log.msgs.items; diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index 1681d068ca..2ab9192bc4 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -3083,5 +3083,5 @@ export fn Bun__resolveEmbeddedNodeFile(vm: *VirtualMachine, in_out_str: *bun.Str export fn ModuleLoader__isBuiltin(data: [*]const u8, len: usize) bool { const str = data[0..len]; - return HardcodedModule.map.get(str) != null; + return HardcodedModule.Alias.bun_aliases.get(str) != null; } diff --git a/src/bun.js/modules/NodeModuleModule.cpp b/src/bun.js/modules/NodeModuleModule.cpp index 8f7444d758..0c9fdb2962 100644 --- a/src/bun.js/modules/NodeModuleModule.cpp +++ b/src/bun.js/modules/NodeModuleModule.cpp @@ -41,8 +41,11 @@ JSC_DECLARE_HOST_FUNCTION(jsFunctionWrap); JSC_DECLARE_CUSTOM_GETTER(getterRequireFunction); JSC_DECLARE_CUSTOM_SETTER(setterRequireFunction); -// This is a mix of bun's builtin module names and also the ones reported by -// node v20.4.0 +// This is a list of builtin module names that do not have the node prefix. It +// also includes Bun's builtin modules, as well as Bun's thirdparty overrides. +// The reason for overstuffing this list is so that uses that use these as the +// 'external' option to a bundler will properly exclude things like 'ws' which +// only work with Bun's native 'ws' implementation and not the JS one on NPM. static constexpr ASCIILiteral builtinModuleNames[] = { "_http_agent"_s, "_http_client"_s, @@ -88,7 +91,6 @@ static constexpr ASCIILiteral builtinModuleNames[] = { "inspector/promises"_s, "module"_s, "net"_s, - "node:test"_s, "os"_s, "path"_s, "path/posix"_s, @@ -409,16 +411,9 @@ JSC_DEFINE_CUSTOM_SETTER(setNodeModuleResolveFilename, return true; } -extern "C" bool ModuleLoader__isBuiltin(const char* data, size_t len); - -struct Parent { - JSArray* paths; - JSString* filename; -}; - -Parent getParent(VM& vm, JSGlobalObject* global, JSValue maybe_parent) +PathResolveModule getParent(VM& vm, JSGlobalObject* global, JSValue maybe_parent) { - Parent value { nullptr, nullptr }; + PathResolveModule value { nullptr, nullptr, false }; if (!maybe_parent) { return value; @@ -461,8 +456,15 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionResolveLookupPaths, return JSC::JSValue::encode(JSC::jsNull()); } - auto parent = getParent(vm, globalObject, callFrame->argument(1)); + PathResolveModule parent = getParent(vm, globalObject, callFrame->argument(1)); RETURN_IF_EXCEPTION(scope, {}); + RELEASE_AND_RETURN(scope, JSC::JSValue::encode(resolveLookupPaths(globalObject, request, parent))); +} + +JSC::JSValue resolveLookupPaths(JSC::JSGlobalObject* globalObject, String request, PathResolveModule parent) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); // Check for node modules paths. if (request.characterAt(0) != '.' || (request.length() > 1 && request.characterAt(1) != '.' && request.characterAt(1) != '/' && @@ -472,16 +474,24 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionResolveLookupPaths, true #endif )) { - auto array = JSC::constructArray( - globalObject, (ArrayAllocationProfile*)nullptr, nullptr, 0); if (parent.paths) { + auto array = JSC::constructArray(globalObject, (ArrayAllocationProfile*)nullptr, nullptr, 0); auto len = parent.paths->length(); for (size_t i = 0; i < len; i++) { auto path = parent.paths->getIndex(globalObject, i); array->push(globalObject, path); } + RELEASE_AND_RETURN(scope, array); + } else if (parent.pathsArrayLazy && parent.filename) { + auto filenameValue = parent.filename->value(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto filename = Bun::toString(filenameValue); + auto paths = JSValue::decode(Resolver__nodeModulePathsJSValue(filename, globalObject, true)); + RELEASE_AND_RETURN(scope, paths); + } else { + auto array = JSC::constructEmptyArray(globalObject, nullptr, 0); + RELEASE_AND_RETURN(scope, array); } - return JSValue::encode(array); } JSValue dirname; @@ -499,9 +509,8 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionResolveLookupPaths, } JSValue values[] = { dirname }; - auto array = JSC::constructArray( - globalObject, (ArrayAllocationProfile*)nullptr, values, 1); - RELEASE_AND_RETURN(scope, JSValue::encode(array)); + auto array = JSC::constructArray(globalObject, (ArrayAllocationProfile*)nullptr, values, 1); + RELEASE_AND_RETURN(scope, array); } extern "C" JSC::EncodedJSValue NodeModuleModule__findPath(JSGlobalObject*, diff --git a/src/bun.js/modules/NodeModuleModule.h b/src/bun.js/modules/NodeModuleModule.h index 56a9513a50..6a412ece5d 100644 --- a/src/bun.js/modules/NodeModuleModule.h +++ b/src/bun.js/modules/NodeModuleModule.h @@ -16,10 +16,21 @@ using namespace Zig; using namespace JSC; - namespace Bun { - JSC_DECLARE_HOST_FUNCTION(jsFunctionIsModuleResolveFilenameSlowPathEnabled); - void addNodeModuleConstructorProperties(JSC::VM &vm, Zig::GlobalObject *globalObject); +JSC_DECLARE_HOST_FUNCTION(jsFunctionIsModuleResolveFilenameSlowPathEnabled); +void addNodeModuleConstructorProperties(JSC::VM &vm, Zig::GlobalObject *globalObject); + +extern "C" JSC::EncodedJSValue Resolver__nodeModulePathsJSValue(BunString specifier, JSC::JSGlobalObject*, bool use_dirname); +extern "C" bool ModuleLoader__isBuiltin(const char* data, size_t len); + +struct PathResolveModule { + JSArray* paths = nullptr; + JSString* filename = nullptr; + /// Derive `paths` from `filename` if needed + bool pathsArrayLazy = false; +}; +JSC::JSValue resolveLookupPaths(JSC::JSGlobalObject* globalObject, String request, PathResolveModule parent); + } namespace Zig { diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts index 8cf2255fde..cc1f8dfc1e 100644 --- a/src/js/builtins.d.ts +++ b/src/js/builtins.d.ts @@ -425,7 +425,7 @@ declare function $requireESM(path: string): any; declare const $requireMap: Map; declare const $internalModuleRegistry: InternalFieldObject; declare function $resolve(name: string, from: string): Promise; -declare function $resolveSync(name: string, from: string, isESM?: boolean, isUserRequireResolve?: boolean): string; +declare function $resolveSync(name: string, from: string, isESM?: boolean, isUserRequireResolve?: boolean, paths?: string[]): string; declare function $resume(): TODO; declare function $search(): TODO; declare function $searchParams(): TODO; diff --git a/src/js/builtins/CommonJS.ts b/src/js/builtins/CommonJS.ts index f0405cc409..98a6cec895 100644 --- a/src/js/builtins/CommonJS.ts +++ b/src/js/builtins/CommonJS.ts @@ -7,7 +7,7 @@ export function main() { // This function is bound when constructing instances of CommonJSModule $visibility = "Private"; -export function require(this: JSCommonJSModule, id: string) { +export function require(this: JSCommonJSModule, _: string) { // Do not use $tailCallForwardArguments here, it causes https://github.com/oven-sh/bun/issues/9225 return $overridableRequire.$apply(this, arguments); } @@ -15,8 +15,8 @@ export function require(this: JSCommonJSModule, id: string) { // overridableRequire can be overridden by setting `Module.prototype.require` $overriddenName = "require"; $visibility = "Private"; -export function overridableRequire(this: JSCommonJSModule, originalId: string) { - const id = $resolveSync(originalId, this.filename, false); +export function overridableRequire(this: JSCommonJSModule, originalId: string, options: { paths?: string[] } = {}) { + const id = $resolveSync(originalId, this.filename, false, false, options ? options.paths : undefined); if (id.startsWith('node:')) { if (id !== originalId) { // A terrible special case where Node.js allows non-prefixed built-ins to @@ -146,8 +146,8 @@ export function overridableRequire(this: JSCommonJSModule, originalId: string) { } $visibility = "Private"; -export function requireResolve(this: string | { filename?: string; id?: string }, id: string) { - return $resolveSync(id, typeof this === "string" ? this : this?.filename ?? this?.id ?? "", false, true); +export function requireResolve(this: string | { filename?: string; id?: string }, id: string, options: { paths?: string[] } = {}) { + return $resolveSync(id, typeof this === "string" ? this : this?.filename ?? this?.id ?? "", false, true, options ? options.paths : undefined); } $visibility = "Private"; diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index aef35bdf38..b8ca0c66aa 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -561,6 +561,13 @@ pub const Resolver = struct { /// over "module" in package.json prefer_module_field: bool = true, + /// This is an array of paths to resolve against. Used for passing an + /// object '{ paths: string[] }' to `require` and `resolve`; This field + /// is overwritten while the resolution happens. + /// + /// When this is null, it is as if it is set to `&.{ path.dirname(referrer) }`. + custom_dir_paths: ?[]const bun.String = null, + pub fn getPackageManager(this: *Resolver) *PackageManager { return this.package_manager orelse brk: { bun.HTTPThread.init(&.{}); @@ -1198,95 +1205,30 @@ pub const Resolver = struct { // Check both relative and package paths for CSS URL tokens, with relative // paths taking precedence over package paths to match Webpack behavior. const is_package_path = kind != .entry_point_run and isPackagePathNotAbsolute(import_path); - var check_relative = !is_package_path or kind.isFromCSS(); - var check_package = is_package_path; + const check_relative = !is_package_path or kind.isFromCSS(); + const check_package = is_package_path; if (check_relative) { - const parts = [_]string{ source_dir, import_path }; - const abs_path = r.fs.absBuf(&parts, bufs(.relative_abs_path)); - - if (r.opts.external.abs_paths.count() > 0 and r.opts.external.abs_paths.contains(abs_path)) { - // If the string literal in the source text is an absolute path and has - // been marked as an external module, mark it as *not* an absolute path. - // That way we preserve the literal text in the output and don't generate - // a relative path from the output directory to that path. - if (r.debug_logs) |*debug| { - debug.addNoteFmt("The path \"{s}\" is marked as external by the user", .{abs_path}); - } - - return .{ - .success = Result{ - .path_pair = .{ .primary = Path.init(r.fs.dirname_store.append(@TypeOf(abs_path), abs_path) catch unreachable) }, - .is_external = true, - }, - }; - } - - // Check the "browser" map - if (r.care_about_browser_field) { - if (r.dirInfoCached(std.fs.path.dirname(abs_path) orelse unreachable) catch null) |_import_dir_info| { - if (_import_dir_info.getEnclosingBrowserScope()) |import_dir_info| { - const pkg = import_dir_info.package_json.?; - if (r.checkBrowserMap( - import_dir_info, - abs_path, - .AbsolutePath, - )) |remap| { - - // Is the path disabled? - if (remap.len == 0) { - var _path = Path.init(r.fs.dirname_store.append(string, abs_path) catch unreachable); - _path.is_disabled = true; - return .{ - .success = Result{ - .path_pair = PathPair{ - .primary = _path, - }, - }, - }; - } - - switch (r.resolveWithoutRemapping(import_dir_info, remap, kind, global_cache)) { - .success => |_result| { - result = Result{ - .path_pair = _result.path_pair, - .diff_case = _result.diff_case, - .dirname_fd = _result.dirname_fd, - .package_json = pkg, - .jsx = r.opts.jsx, - .module_type = _result.module_type, - .is_external = _result.is_external, - .is_external_and_rewrite_import_path = _result.is_external, - }; - check_relative = false; - check_package = false; - }, - else => {}, - } - } + if (r.custom_dir_paths) |custom_paths| { + @branchHint(.unlikely); + for (custom_paths) |custom_path| { + const custom_utf8 = custom_path.toUTF8WithoutRef(bun.default_allocator); + defer custom_utf8.deinit(); + switch (r.checkRelativePath(custom_utf8.slice(), import_path, kind, global_cache)) { + .success => |res| return .{ .success = res }, + .pending => |p| return .{ .pending = p }, + .failure => |p| return .{ .failure = p }, + .not_found => {}, } } - } - - if (check_relative) { - const prev_extension_order = r.extension_order; - defer { - r.extension_order = prev_extension_order; - } - if (strings.pathContainsNodeModulesFolder(abs_path)) { - r.extension_order = r.opts.extension_order.kind(kind, true); - } - if (r.loadAsFileOrDirectory(abs_path, kind)) |res| { - check_package = false; - result = Result{ - .path_pair = res.path_pair, - .diff_case = res.diff_case, - .dirname_fd = res.dirname_fd, - .package_json = res.package_json, - .jsx = r.opts.jsx, - }; - } else if (!check_package) { - return .{ .not_found = {} }; + bun.debugAssert(!check_package); // always from JavaScript + return .{ .not_found = {} }; // bail out now since there isn't anywhere else to check + } else { + switch (r.checkRelativePath(source_dir, import_path, kind, global_cache)) { + .success => |res| return .{ .success = res }, + .pending => |p| return .{ .pending = p }, + .failure => |p| return .{ .failure = p }, + .not_found => {}, } } } @@ -1363,174 +1305,276 @@ pub const Resolver = struct { } } - var source_dir_info = r.dirInfoCached(source_dir) catch (return .{ .not_found = {} }) orelse dir: { - // It is possible to resolve with a source file that does not exist: - // A. Bundler plugin refers to a non-existing `resolveDir`. - // B. `createRequire()` is called with a path that does not exist. This was - // hit in Nuxt, specifically the `vite-node` dependency [1]. - // - // Normally it would make sense to always bail here, but in the case of - // resolving "hello" from "/project/nonexistent_dir/index.ts", resolution - // should still query "/project/node_modules" and "/node_modules" - // - // For case B in Node.js, they use `_resolveLookupPaths` in - // combination with `_nodeModulePaths` to collect a listing of - // all possible parent `node_modules` [2]. Bun has a much smarter - // approach that caches directory entries, but it (correctly) does - // not cache non-existing directories. To successfully resolve this, - // Bun finds the nearest existing directory, and uses that as the base - // for `node_modules` resolution. Since that directory entry knows how - // to resolve concrete node_modules, this iteration stops at the first - // existing directory, regardless of what it is. - // - // The resulting `source_dir_info` cannot resolve relative files. - // - // [1]: https://github.com/oven-sh/bun/issues/16705 - // [2]: https://github.com/nodejs/node/blob/e346323109b49fa6b9a4705f4e3816fc3a30c151/lib/internal/modules/cjs/loader.js#L1934 - if (Environment.allow_assert) - bun.assert(isPackagePath(import_path)); - var closest_dir = source_dir; - // Use std.fs.path.dirname to get `null` once the entire - // directory tree has been visited. `null` is theoretically - // impossible since the drive root should always exist. - while (std.fs.path.dirname(closest_dir)) |current| : (closest_dir = current) { - if (r.dirInfoCached(current) catch return .{ .not_found = {} }) |dir| - break :dir dir; - } - return .{ .not_found = {} }; - }; - - if (r.care_about_browser_field) { - // Support remapping one package path to another via the "browser" field - if (source_dir_info.getEnclosingBrowserScope()) |browser_scope| { - if (browser_scope.package_json) |package_json| { - if (r.checkBrowserMap( - browser_scope, - import_path, - .PackagePath, - )) |remapped| { - if (remapped.len == 0) { - // "browser": {"module": false} - // does the module exist in the filesystem? - switch (r.loadNodeModules(import_path, kind, source_dir_info, global_cache, false)) { - .success => |node_module| { - var pair = node_module.path_pair; - pair.primary.is_disabled = true; - if (pair.secondary != null) { - pair.secondary.?.is_disabled = true; - } - return .{ - .success = Result{ - .path_pair = pair, - .dirname_fd = node_module.dirname_fd, - .diff_case = node_module.diff_case, - .package_json = package_json, - .jsx = r.opts.jsx, - }, - }; - }, - else => { - // "browser": {"module": false} - // the module doesn't exist and it's disabled - // so we should just not try to load it - var primary = Path.init(import_path); - primary.is_disabled = true; - return .{ - .success = Result{ - .path_pair = PathPair{ .primary = primary }, - .diff_case = null, - .jsx = r.opts.jsx, - }, - }; - }, - } - } - - import_path = remapped; - source_dir_info = browser_scope; - } + if (r.custom_dir_paths) |custom_paths| { + @branchHint(.unlikely); + for (custom_paths) |custom_path| { + const custom_utf8 = custom_path.toUTF8WithoutRef(bun.default_allocator); + defer custom_utf8.deinit(); + switch (r.checkPackagePath(custom_utf8.slice(), import_path, kind, global_cache)) { + .success => |res| return .{ .success = res }, + .pending => |p| return .{ .pending = p }, + .failure => |p| return .{ .failure = p }, + .not_found => {}, } } - } - - switch (r.resolveWithoutRemapping(source_dir_info, import_path, kind, global_cache)) { - .success => |res| { - result.path_pair = res.path_pair; - result.dirname_fd = res.dirname_fd; - result.file_fd = res.file_fd; - result.package_json = res.package_json; - result.diff_case = res.diff_case; - result.is_from_node_modules = result.is_from_node_modules or res.is_node_module; - result.jsx = r.opts.jsx; - result.module_type = res.module_type; - result.is_external = res.is_external; - // Potentially rewrite the import path if it's external that - // was remapped to a different path - result.is_external_and_rewrite_import_path = result.is_external; - - if (res.path_pair.primary.is_disabled and res.path_pair.secondary == null) { - return .{ .success = result }; - } - - if (res.package_json != null and r.care_about_browser_field) { - var base_dir_info = res.dir_info orelse (r.readDirInfo(res.path_pair.primary.name.dir) catch null) orelse return .{ .success = result }; - if (base_dir_info.getEnclosingBrowserScope()) |browser_scope| { - if (r.checkBrowserMap( - browser_scope, - res.path_pair.primary.text, - .AbsolutePath, - )) |remap| { - if (remap.len == 0) { - result.path_pair.primary.is_disabled = true; - result.path_pair.primary = Fs.Path.initWithNamespace(remap, "file"); - } else { - switch (r.resolveWithoutRemapping(browser_scope, remap, kind, global_cache)) { - .success => |remapped| { - result.path_pair = remapped.path_pair; - result.dirname_fd = remapped.dirname_fd; - result.file_fd = remapped.file_fd; - result.package_json = remapped.package_json; - result.diff_case = remapped.diff_case; - result.module_type = remapped.module_type; - result.is_external = remapped.is_external; - - // Potentially rewrite the import path if it's external that - // was remapped to a different path - result.is_external_and_rewrite_import_path = result.is_external; - - result.is_from_node_modules = result.is_from_node_modules or remapped.is_node_module; - return .{ .success = result }; - }, - else => {}, - } - } - } - } - } - - return .{ .success = result }; - }, - .pending => |p| return .{ .pending = p }, - .failure => |p| return .{ .failure = p }, - else => return .{ .not_found = {} }, + } else { + switch (r.checkPackagePath(source_dir, import_path, kind, global_cache)) { + .success => |res| return .{ .success = res }, + .pending => |p| return .{ .pending = p }, + .failure => |p| return .{ .failure = p }, + .not_found => {}, + } } } - return .{ .success = result }; + return .{ .not_found = {} }; } - pub fn packageJSONForResolvedNodeModule( - r: *ThisResolver, - result: *const Result, - ) ?*const PackageJSON { - return @call(bun.callmod_inline, packageJSONForResolvedNodeModuleWithIgnoreMissingName, .{ r, result, true }); + pub fn checkRelativePath(r: *ThisResolver, source_dir: string, import_path: string, kind: ast.ImportKind, global_cache: GlobalCache) Result.Union { + const parts = [_]string{ source_dir, import_path }; + const abs_path = r.fs.absBuf(&parts, bufs(.relative_abs_path)); + + if (r.opts.external.abs_paths.count() > 0 and r.opts.external.abs_paths.contains(abs_path)) { + // If the string literal in the source text is an absolute path and has + // been marked as an external module, mark it as *not* an absolute path. + // That way we preserve the literal text in the output and don't generate + // a relative path from the output directory to that path. + if (r.debug_logs) |*debug| { + debug.addNoteFmt("The path \"{s}\" is marked as external by the user", .{abs_path}); + } + + return .{ .success = .{ + .path_pair = .{ .primary = Path.init(r.fs.dirname_store.append(@TypeOf(abs_path), abs_path) catch bun.outOfMemory()) }, + .is_external = true, + } }; + } + + // Check the "browser" map + if (r.care_about_browser_field) { + if (r.dirInfoCached(std.fs.path.dirname(abs_path) orelse unreachable) catch null) |_import_dir_info| { + if (_import_dir_info.getEnclosingBrowserScope()) |import_dir_info| { + const pkg = import_dir_info.package_json.?; + if (r.checkBrowserMap( + import_dir_info, + abs_path, + .AbsolutePath, + )) |remap| { + // Is the path disabled? + if (remap.len == 0) { + var _path = Path.init(r.fs.dirname_store.append(string, abs_path) catch unreachable); + _path.is_disabled = true; + return .{ + .success = Result{ + .path_pair = PathPair{ + .primary = _path, + }, + }, + }; + } + + switch (r.resolveWithoutRemapping(import_dir_info, remap, kind, global_cache)) { + .success => |result| { + return .{ .success = .{ + .path_pair = result.path_pair, + .diff_case = result.diff_case, + .dirname_fd = result.dirname_fd, + .package_json = pkg, + .jsx = r.opts.jsx, + .module_type = result.module_type, + .is_external = result.is_external, + .is_external_and_rewrite_import_path = result.is_external, + } }; + }, + else => {}, + } + } + } + } + } + + const prev_extension_order = r.extension_order; + defer r.extension_order = prev_extension_order; + if (strings.pathContainsNodeModulesFolder(abs_path)) { + r.extension_order = r.opts.extension_order.kind(kind, true); + } + if (r.loadAsFileOrDirectory(abs_path, kind)) |res| { + return .{ .success = .{ + .path_pair = res.path_pair, + .diff_case = res.diff_case, + .dirname_fd = res.dirname_fd, + .package_json = res.package_json, + .jsx = r.opts.jsx, + } }; + } else { + return .{ .not_found = {} }; + } + } + + pub fn checkPackagePath(r: *ThisResolver, source_dir: string, unremapped_import_path: string, kind: ast.ImportKind, global_cache: GlobalCache) Result.Union { + var import_path = unremapped_import_path; + var source_dir_info = r.dirInfoCached(source_dir) catch (return .{ .not_found = {} }) orelse dir: { + // It is possible to resolve with a source file that does not exist: + // A. Bundler plugin refers to a non-existing `resolveDir`. + // B. `createRequire()` is called with a path that does not exist. This was + // hit in Nuxt, specifically the `vite-node` dependency [1]. + // + // Normally it would make sense to always bail here, but in the case of + // resolving "hello" from "/project/nonexistent_dir/index.ts", resolution + // should still query "/project/node_modules" and "/node_modules" + // + // For case B in Node.js, they use `_resolveLookupPaths` in + // combination with `_nodeModulePaths` to collect a listing of + // all possible parent `node_modules` [2]. Bun has a much smarter + // approach that caches directory entries, but it (correctly) does + // not cache non-existing directories. To successfully resolve this, + // Bun finds the nearest existing directory, and uses that as the base + // for `node_modules` resolution. Since that directory entry knows how + // to resolve concrete node_modules, this iteration stops at the first + // existing directory, regardless of what it is. + // + // The resulting `source_dir_info` cannot resolve relative files. + // + // [1]: https://github.com/oven-sh/bun/issues/16705 + // [2]: https://github.com/nodejs/node/blob/e346323109b49fa6b9a4705f4e3816fc3a30c151/lib/internal/modules/cjs/loader.js#L1934 + if (Environment.allow_assert) + bun.assert(isPackagePath(import_path)); + var closest_dir = source_dir; + // Use std.fs.path.dirname to get `null` once the entire + // directory tree has been visited. `null` is theoretically + // impossible since the drive root should always exist. + while (std.fs.path.dirname(closest_dir)) |current| : (closest_dir = current) { + if (r.dirInfoCached(current) catch return .{ .not_found = {} }) |dir| + break :dir dir; + } + return .{ .not_found = {} }; + }; + + if (r.care_about_browser_field) { + // Support remapping one package path to another via the "browser" field + if (source_dir_info.getEnclosingBrowserScope()) |browser_scope| { + if (browser_scope.package_json) |package_json| { + if (r.checkBrowserMap( + browser_scope, + import_path, + .PackagePath, + )) |remapped| { + if (remapped.len == 0) { + // "browser": {"module": false} + // does the module exist in the filesystem? + switch (r.loadNodeModules(import_path, kind, source_dir_info, global_cache, false)) { + .success => |node_module| { + var pair = node_module.path_pair; + pair.primary.is_disabled = true; + if (pair.secondary != null) { + pair.secondary.?.is_disabled = true; + } + return .{ + .success = Result{ + .path_pair = pair, + .dirname_fd = node_module.dirname_fd, + .diff_case = node_module.diff_case, + .package_json = package_json, + .jsx = r.opts.jsx, + }, + }; + }, + else => { + // "browser": {"module": false} + // the module doesn't exist and it's disabled + // so we should just not try to load it + var primary = Path.init(import_path); + primary.is_disabled = true; + return .{ + .success = Result{ + .path_pair = PathPair{ .primary = primary }, + .diff_case = null, + .jsx = r.opts.jsx, + }, + }; + }, + } + } + + import_path = remapped; + source_dir_info = browser_scope; + } + } + } + } + + switch (r.resolveWithoutRemapping(source_dir_info, import_path, kind, global_cache)) { + .success => |res| { + var result: Result = Result{ + .path_pair = PathPair{ + .primary = Path.empty, + }, + .jsx = r.opts.jsx, + }; + result.path_pair = res.path_pair; + result.dirname_fd = res.dirname_fd; + result.file_fd = res.file_fd; + result.package_json = res.package_json; + result.diff_case = res.diff_case; + result.is_from_node_modules = result.is_from_node_modules or res.is_node_module; + result.jsx = r.opts.jsx; + result.module_type = res.module_type; + result.is_external = res.is_external; + // Potentially rewrite the import path if it's external that + // was remapped to a different path + result.is_external_and_rewrite_import_path = result.is_external; + + if (res.path_pair.primary.is_disabled and res.path_pair.secondary == null) { + return .{ .success = result }; + } + + if (res.package_json != null and r.care_about_browser_field) { + var base_dir_info = res.dir_info orelse (r.readDirInfo(res.path_pair.primary.name.dir) catch null) orelse return .{ .success = result }; + if (base_dir_info.getEnclosingBrowserScope()) |browser_scope| { + if (r.checkBrowserMap( + browser_scope, + res.path_pair.primary.text, + .AbsolutePath, + )) |remap| { + if (remap.len == 0) { + result.path_pair.primary.is_disabled = true; + result.path_pair.primary = Fs.Path.initWithNamespace(remap, "file"); + } else { + switch (r.resolveWithoutRemapping(browser_scope, remap, kind, global_cache)) { + .success => |remapped| { + result.path_pair = remapped.path_pair; + result.dirname_fd = remapped.dirname_fd; + result.file_fd = remapped.file_fd; + result.package_json = remapped.package_json; + result.diff_case = remapped.diff_case; + result.module_type = remapped.module_type; + result.is_external = remapped.is_external; + + // Potentially rewrite the import path if it's external that + // was remapped to a different path + result.is_external_and_rewrite_import_path = result.is_external; + + result.is_from_node_modules = result.is_from_node_modules or remapped.is_node_module; + return .{ .success = result }; + }, + else => {}, + } + } + } + } + } + + return .{ .success = result }; + }, + .pending => |p| return .{ .pending = p }, + .failure => |p| return .{ .failure = p }, + else => return .{ .not_found = {} }, + } } // This is a fallback, hopefully not called often. It should be relatively quick because everything should be in the cache. - fn packageJSONForResolvedNodeModuleWithIgnoreMissingName( + pub fn packageJSONForResolvedNodeModule( r: *ThisResolver, result: *const Result, - comptime ignore_missing_name: bool, ) ?*const PackageJSON { var dir_info = (r.dirInfoCached(result.path_pair.primary.name.dir) catch null) orelse return null; while (true) { @@ -1538,13 +1582,7 @@ pub const Resolver = struct { // if it doesn't have a name, assume it's something just for adjusting the main fields (react-bootstrap does this) // In that case, we really would like the top-level package that you download from NPM // so we ignore any unnamed packages - if (comptime !ignore_missing_name) { - if (pkg.name.len > 0) { - return pkg; - } - } else { - return pkg; - } + return pkg; } dir_info = dir_info.getParent() orelse return null; @@ -3389,11 +3427,7 @@ pub const Resolver = struct { }; } - comptime { - const Resolver__nodeModulePathsForJS = JSC.toJSHostFunction(Resolver__nodeModulePathsForJS_); - @export(&Resolver__nodeModulePathsForJS, .{ .name = "Resolver__nodeModulePathsForJS" }); - } - pub fn Resolver__nodeModulePathsForJS_(globalThis: *bun.JSC.JSGlobalObject, callframe: *bun.JSC.CallFrame) bun.JSError!JSC.JSValue { + pub fn nodeModulePathsForJS(globalThis: *bun.JSC.JSGlobalObject, callframe: *bun.JSC.CallFrame) bun.JSError!JSC.JSValue { bun.JSC.markBinding(@src()); const argument: bun.JSC.JSValue = callframe.argument(0); @@ -3403,17 +3437,17 @@ pub const Resolver = struct { const in_str = try argument.toBunString(globalThis); defer in_str.deref(); - return nodeModulePathsJSValue(in_str, globalThis); + return nodeModulePathsJSValue(in_str, globalThis, false); } pub export fn Resolver__propForRequireMainPaths(globalThis: *bun.JSC.JSGlobalObject) callconv(.C) JSC.JSValue { bun.JSC.markBinding(@src()); const in_str = bun.String.init("."); - return nodeModulePathsJSValue(in_str, globalThis); + return nodeModulePathsJSValue(in_str, globalThis, false); } - pub fn nodeModulePathsJSValue(in_str: bun.String, globalObject: *bun.JSC.JSGlobalObject) bun.JSC.JSValue { + pub fn nodeModulePathsJSValue(in_str: bun.String, globalObject: *bun.JSC.JSGlobalObject, use_dirname: bool) callconv(.C) bun.JSC.JSValue { var arena = std.heap.ArenaAllocator.init(bun.default_allocator); defer arena.deinit(); var stack_fallback_allocator = std.heap.stackFallback(1024, arena.allocator()); @@ -3424,12 +3458,13 @@ pub const Resolver = struct { const sliced = in_str.toUTF8(bun.default_allocator); defer sliced.deinit(); + const base_path = if (use_dirname) std.fs.path.dirname(sliced.slice()) orelse sliced.slice() else sliced.slice(); const buf = bufs(.node_modules_paths_buf); const full_path = bun.path.joinAbsStringBuf( bun.fs.FileSystem.instance.top_level_dir, buf, - &.{sliced.slice()}, + &.{base_path}, .auto, ); const root_index = switch (bun.Environment.os) { @@ -4320,6 +4355,8 @@ pub const GlobalCache = enum { comptime { _ = Resolver.Resolver__propForRequireMainPaths; + @export(&JSC.toJSHostFunction(Resolver.nodeModulePathsForJS), .{ .name = "Resolver__nodeModulePathsForJS" }); + @export(&Resolver.nodeModulePathsJSValue, .{ .name = "Resolver__nodeModulePathsJSValue" }); } const assert = bun.assert; diff --git a/test/js/bun/resolve/load-same-js-file-a-lot.test.ts b/test/js/bun/resolve/load-same-js-file-a-lot.test.ts index face20c5c0..6c82d8bd2e 100644 --- a/test/js/bun/resolve/load-same-js-file-a-lot.test.ts +++ b/test/js/bun/resolve/load-same-js-file-a-lot.test.ts @@ -1,4 +1,5 @@ import { expect, test } from "bun:test"; +import { isDebug } from "harness"; test("load the same file 10,000 times", async () => { const meta = { @@ -24,7 +25,7 @@ test("load the same file 10,000 times", async () => { } Bun.gc(true); Bun.unsafe.gcAggressionLevel(prev); -}); +}, isDebug ? 20_000 : 5000); test("load the same empty JS file 10,000 times", async () => { const prev = Bun.unsafe.gcAggressionLevel(); diff --git a/test/js/node/module/node-module-module.test.js b/test/js/node/module/node-module-module.test.js index 2aadb745da..6f80829b81 100644 --- a/test/js/node/module/node-module-module.test.js +++ b/test/js/node/module/node-module-module.test.js @@ -5,7 +5,7 @@ import path from "path"; test("builtinModules exists", () => { expect(Array.isArray(builtinModules)).toBe(true); - expect(builtinModules).toHaveLength(77); + expect(builtinModules).toHaveLength(76); }); test("isBuiltin() works", () => { diff --git a/test/js/node/test/parallel/test-inspector-strip-types.js b/test/js/node/test/parallel/test-inspector-strip-types.js deleted file mode 100644 index 6792221acf..0000000000 --- a/test/js/node/test/parallel/test-inspector-strip-types.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; - -const common = require('../common'); -common.skipIfInspectorDisabled(); -if (!process.config.variables.node_use_amaro) common.skip('Requires Amaro'); - -const { NodeInstance } = require('../common/inspector-helper.js'); -const fixtures = require('../common/fixtures'); -const assert = require('assert'); -const { pathToFileURL } = require('url'); - -const scriptPath = fixtures.path('typescript/ts/test-typescript.ts'); -const scriptURL = pathToFileURL(scriptPath); - -async function runTest() { - const child = new NodeInstance( - ['--inspect-brk=0'], - undefined, - scriptPath); - - const session = await child.connectInspectorSession(); - - await session.send({ method: 'NodeRuntime.enable' }); - await session.waitForNotification('NodeRuntime.waitingForDebugger'); - await session.send([ - { 'method': 'Debugger.enable' }, - { 'method': 'Runtime.enable' }, - { 'method': 'Runtime.runIfWaitingForDebugger' }, - ]); - await session.send({ method: 'NodeRuntime.disable' }); - - const scriptParsed = await session.waitForNotification((notification) => { - if (notification.method !== 'Debugger.scriptParsed') return false; - - return notification.params.url === scriptPath || notification.params.url === scriptURL.href; - }); - // Verify that the script has a sourceURL, hinting that it is a generated source. - assert(scriptParsed.params.hasSourceURL || common.isInsideDirWithUnusualChars); - - await session.waitForPauseOnStart(); - await session.runToCompletion(); - - assert.strictEqual((await child.expectShutdown()).exitCode, 0); -} - -runTest().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-module-strip-types.js b/test/js/node/test/parallel/test-module-strip-types.js deleted file mode 100644 index 0f90039b56..0000000000 --- a/test/js/node/test/parallel/test-module-strip-types.js +++ /dev/null @@ -1,105 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!process.config.variables.node_use_amaro) common.skip('Requires Amaro'); -const assert = require('assert'); -const vm = require('node:vm'); -const { stripTypeScriptTypes } = require('node:module'); -const { test } = require('node:test'); - -common.expectWarning( - 'ExperimentalWarning', - 'stripTypeScriptTypes is an experimental feature and might change at any time', -); - -const sourceToBeTransformed = ` - namespace MathUtil { - export const add = (a: number, b: number) => a + b; - }`; -const sourceToBeTransformedMapping = 'UACY;aACK,MAAM,CAAC,GAAW,IAAc,IAAI;AACnD,GAFU,aAAA'; - -test('stripTypeScriptTypes', () => { - const source = 'const x: number = 1;'; - const result = stripTypeScriptTypes(source); - assert.strictEqual(result, 'const x = 1;'); -}); - -test('stripTypeScriptTypes explicit', () => { - const source = 'const x: number = 1;'; - const result = stripTypeScriptTypes(source, { mode: 'strip' }); - assert.strictEqual(result, 'const x = 1;'); -}); - -test('stripTypeScriptTypes code is not a string', () => { - assert.throws(() => stripTypeScriptTypes({}), - { code: 'ERR_INVALID_ARG_TYPE' }); -}); - -test('stripTypeScriptTypes invalid mode', () => { - const source = 'const x: number = 1;'; - assert.throws(() => stripTypeScriptTypes(source, { mode: 'invalid' }), { code: 'ERR_INVALID_ARG_VALUE' }); -}); - -test('stripTypeScriptTypes sourceMap throws when mode is strip', () => { - const source = 'const x: number = 1;'; - assert.throws(() => stripTypeScriptTypes(source, - { mode: 'strip', sourceMap: true }), - { code: 'ERR_INVALID_ARG_VALUE' }); -}); - -test('stripTypeScriptTypes sourceUrl throws when mode is strip', () => { - const source = 'const x: number = 1;'; - const result = stripTypeScriptTypes(source, { mode: 'strip', sourceUrl: 'foo.ts' }); - assert.strictEqual(result, 'const x = 1;\n\n//# sourceURL=foo.ts'); -}); - -test('stripTypeScriptTypes source map when mode is transform', () => { - const result = stripTypeScriptTypes(sourceToBeTransformed, { mode: 'transform', sourceMap: true }); - const script = new vm.Script(result); - const sourceMap = - { - version: 3, - sources: [''], - names: [], - mappings: sourceToBeTransformedMapping, - }; - const inlinedSourceMap = Buffer.from(JSON.stringify(sourceMap)).toString('base64'); - assert.strictEqual(script.sourceMapURL, `data:application/json;base64,${inlinedSourceMap}`); -}); - -test('stripTypeScriptTypes source map when mode is transform and sourceUrl', () => { - const result = stripTypeScriptTypes(sourceToBeTransformed, { - mode: 'transform', - sourceMap: true, - sourceUrl: 'test.ts' - }); - const script = new vm.Script(result); - const sourceMap = - { - version: 3, - sources: ['test.ts'], - names: [], - mappings: sourceToBeTransformedMapping, - }; - const inlinedSourceMap = Buffer.from(JSON.stringify(sourceMap)).toString('base64'); - assert.strictEqual(script.sourceMapURL, `data:application/json;base64,${inlinedSourceMap}`); -}); - -test('stripTypeScriptTypes source map when mode is transform and sourceUrl with non-latin-1 chars', () => { - const sourceUrl = 'dir%20with $unusual"chars?\'åß∂ƒ©∆¬…`.cts'; - const result = stripTypeScriptTypes(sourceToBeTransformed, { - mode: 'transform', - sourceMap: true, - sourceUrl, - }); - const script = new vm.Script(result); - const sourceMap = - { - version: 3, - sources: [sourceUrl], - names: [], - mappings: sourceToBeTransformedMapping, - }; - const inlinedSourceMap = Buffer.from(JSON.stringify(sourceMap)).toString('base64'); - assert.strictEqual(script.sourceMapURL, `data:application/json;base64,${inlinedSourceMap}`); -}); diff --git a/test/js/node/test/test-require-resolve.js b/test/js/node/test/test-require-resolve.js new file mode 100644 index 0000000000..6aec57189e --- /dev/null +++ b/test/js/node/test/test-require-resolve.js @@ -0,0 +1,105 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const { builtinModules } = require('module'); +const path = require('path'); + +assert.strictEqual( + require.resolve(fixtures.path('a')).toLowerCase(), + fixtures.path('a.js').toLowerCase()); +assert.strictEqual( + require.resolve(fixtures.path('nested-index', 'one')).toLowerCase(), + fixtures.path('nested-index', 'one', 'index.js').toLowerCase()); +assert.strictEqual(require.resolve('path'), 'path'); + +// Test configurable resolve() paths. +require(fixtures.path('require-resolve.js')); +require(fixtures.path('resolve-paths', 'default', 'verify-paths.js')); + +[1, false, null, undefined, {}].forEach((value) => { + const message = 'The "request" argument must be of type string.' + + common.invalidArgTypeHelper(value); + assert.throws( + () => { require.resolve(value); }, + { + code: 'ERR_INVALID_ARG_TYPE', + message + }); + + assert.throws( + () => { require.resolve.paths(value); }, + { + code: 'ERR_INVALID_ARG_TYPE', + message + }); +}); + +// Test require.resolve.paths. +{ + // builtinModules. + builtinModules.forEach((mod) => { + if (mod.startsWith('bun')) return; + + // TODO(@jasnell): Remove once node:quic is no longer flagged + if (mod === 'node:quic') return; + assert.strictEqual(require.resolve.paths(mod), null, `require.resolve.paths(${mod}) should return null`); + if (!mod.startsWith('node:')) { + try { + require.resolve(`node:${mod}`); + } catch (e) { + return; // skip modules that don't support the node prefix, such as 'bun:ffi' -> 'node:bun:ffi' + } + + assert.strictEqual(require.resolve.paths(`node:${mod}`), null, `require.resolve.paths(node:${mod}) should return null`); + } + }); + + // node_modules. + const resolvedPaths = require.resolve.paths('eslint'); + assert.strictEqual(Array.isArray(resolvedPaths), true); + assert.strictEqual(resolvedPaths[0].includes('node_modules'), true); + + // relativeModules. + const relativeModules = ['.', '..', './foo', '../bar']; + relativeModules.forEach((mod) => { + const resolvedPaths = require.resolve.paths(mod); + assert.strictEqual(Array.isArray(resolvedPaths), true); + assert.strictEqual(resolvedPaths.length, 1); + assert.strictEqual(resolvedPaths[0], path.dirname(__filename)); + + // Shouldn't look up relative modules from 'node_modules'. + assert.strictEqual(resolvedPaths.includes('/node_modules'), false); + }); +} + +{ + assert.strictEqual(require.resolve('node:test'), 'node:test'); + assert.strictEqual(require.resolve('node:fs'), 'node:fs'); + + assert.throws( + () => require.resolve('node:unknown'), + { code: 'MODULE_NOT_FOUND' }, + ); +}