diff --git a/docs/runtime/nodejs-apis.md b/docs/runtime/nodejs-apis.md index 198cad6785..6c2ce4b7f6 100644 --- a/docs/runtime/nodejs-apis.md +++ b/docs/runtime/nodejs-apis.md @@ -118,7 +118,7 @@ Some methods are not optimized yet. ### [`node:module`](https://nodejs.org/api/module.html) -🟡 Missing `runMain` `syncBuiltinESMExports`, `Module#load()`. Overriding `require.cache` is supported for ESM & CJS modules. `module._extensions`, `module._pathCache`, `module._cache` are no-ops. `module.register` is not implemented and we recommend using a [`Bun.plugin`](https://bun.sh/docs/runtime/plugins) in the meantime. +🟡 Missing `syncBuiltinESMExports`, `Module#load()`. Overriding `require.cache` is supported for ESM & CJS modules. `module._extensions`, `module._pathCache`, `module._cache` are no-ops. `module.register` is not implemented and we recommend using a [`Bun.plugin`](https://bun.sh/docs/runtime/plugins) in the meantime. ### [`node:net`](https://nodejs.org/api/net.html) diff --git a/src/bun.js/bindings/CommonJSModuleRecord.cpp b/src/bun.js/bindings/CommonJSModuleRecord.cpp index 91489a844f..02a5450f53 100644 --- a/src/bun.js/bindings/CommonJSModuleRecord.cpp +++ b/src/bun.js/bindings/CommonJSModuleRecord.cpp @@ -257,12 +257,22 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionEvaluateCommonJSModule, (JSGlobalObject * lex RELEASE_AND_RETURN(throwScope, JSValue::encode(jsUndefined())); } - UNUSED_PARAM(referrer); + JSValue returnValue = jsNull(); + if (LIKELY(referrer)) { + if (UNLIKELY(referrer->m_childrenValue)) { + // It's too hard to append from native code: + // referrer.children.indexOf(moduleObject) === -1 && referrer.children.push(moduleObject) + returnValue = referrer->m_childrenValue.get(); + } else { + referrer->m_children.append(WriteBarrier()); + referrer->m_children.last().set(vm, referrer, moduleObject); + } + } moduleObject->load(vm, globalObject); RETURN_IF_EXCEPTION(throwScope, {}); - RELEASE_AND_RETURN(throwScope, JSValue::encode(jsUndefined())); + RELEASE_AND_RETURN(throwScope, JSValue::encode(returnValue)); } JSC_DEFINE_HOST_FUNCTION(requireResolvePathsFunction, (JSGlobalObject * globalObject, CallFrame* callframe)) @@ -450,6 +460,62 @@ JSC_DEFINE_CUSTOM_GETTER(getterPaths, (JSC::JSGlobalObject * globalObject, JSC:: return JSValue::encode(thisObject->m_paths.get()); } +JSC_DEFINE_CUSTOM_SETTER(setterChildren, + (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, + JSC::EncodedJSValue value, JSC::PropertyName propertyName)) +{ + JSCommonJSModule* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (!thisObject) + return false; + thisObject->m_children.clear(); + thisObject->m_childrenValue.set(globalObject->vm(), thisObject, JSValue::decode(value)); + return true; +} + +JSC_DEFINE_CUSTOM_GETTER(getterChildren, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + JSCommonJSModule* mod = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!mod)) { + return JSValue::encode(jsUndefined()); + } + + if (!mod->m_childrenValue) { + auto throwScope = DECLARE_THROW_SCOPE(globalObject->vm()); + MarkedArgumentBuffer children; + children.ensureCapacity(mod->m_children.size()); + + // Deduplicate children while preserving insertion order. + JSCommonJSModule* last = nullptr; + int n = -1; + for (WriteBarrier childBarrier : mod->m_children) { + JSCommonJSModule* child = jsCast(childBarrier.get()); + // Check the last module since duplicate imports, if any, will + // probably be adjacent. Then just do a linear scan. + if (UNLIKELY(last == child)) continue; + int i = 0; + while (i < n) { + if (UNLIKELY(children.at(i).asCell() == child)) goto next; + i += 1; + } + children.append(child); + last = child; + n += 1; + next: { + } + } + + // Construct the array + JSArray* array = JSC::constructArray(globalObject, static_cast(nullptr), children); + mod->m_childrenValue.set(globalObject->vm(), mod, array); + + mod->m_children.clear(); + + return JSValue::encode(array); + } + + return JSValue::encode(mod->m_childrenValue.get()); +} + JSC_DEFINE_CUSTOM_GETTER(getterLoaded, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) { JSCommonJSModule* thisObject = jsDynamicCast(JSValue::decode(thisValue)); @@ -510,7 +576,6 @@ JSC_DEFINE_CUSTOM_SETTER(setterParent, thisObject->m_overriddenParent.clear(); } else { thisObject->m_parent = {}; - thisObject->m_overriddenParent.set(globalObject->vm(), thisObject, JSValue::decode(value)); } return true; @@ -528,11 +593,6 @@ JSC_DEFINE_CUSTOM_SETTER(setterLoaded, return true; } -static JSValue createChildren(VM& vm, JSObject* object) -{ - return constructEmptyArray(object->globalObject(), nullptr, 0); -} - JSC_DEFINE_HOST_FUNCTION(functionCommonJSModuleRecord_compile, (JSGlobalObject * globalObject, CallFrame* callframe)) { auto* moduleObject = jsDynamicCast(callframe->thisValue()); @@ -596,7 +656,7 @@ JSC_DEFINE_HOST_FUNCTION(functionCommonJSModuleRecord_compile, (JSGlobalObject * static const struct HashTableValue JSCommonJSModulePrototypeTableValues[] = { { "_compile"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, functionCommonJSModuleRecord_compile, 2 } }, - { "children"_s, static_cast(PropertyAttribute::PropertyCallback), NoIntrinsic, { HashTableValue::LazyPropertyType, createChildren } }, + { "children"_s, static_cast(PropertyAttribute::CustomAccessor | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, getterChildren, setterChildren } }, { "filename"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, getterFilename, setterFilename } }, { "id"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, getterId, setterId } }, { "loaded"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, getterLoaded, setterLoaded } }, @@ -1016,6 +1076,8 @@ void JSCommonJSModule::visitChildrenImpl(JSCell* cell, Visitor& visitor) visitor.appendHidden(thisObject->m_dirname); visitor.appendHidden(thisObject->m_paths); visitor.appendHidden(thisObject->m_overriddenParent); + visitor.appendHidden(thisObject->m_childrenValue); + visitor.appendValues(thisObject->m_children.data(), thisObject->m_children.size()); } DEFINE_VISIT_CHILDREN(JSCommonJSModule); diff --git a/src/bun.js/bindings/CommonJSModuleRecord.h b/src/bun.js/bindings/CommonJSModuleRecord.h index a7a1400fff..76e714b91a 100644 --- a/src/bun.js/bindings/CommonJSModuleRecord.h +++ b/src/bun.js/bindings/CommonJSModuleRecord.h @@ -44,6 +44,17 @@ public: mutable JSC::WriteBarrier m_dirname; // Initialized lazily; can be overridden. mutable JSC::WriteBarrier m_paths; + // Children must always be tracked in case the script decides to access + // `module.children`. In that case, all children may also need their + // children fields to exist, recursively. To avoid allocating a *JSArray for + // each module, the children array is constructed internally as a + // Vector of pointers. If accessed, deduplication happens and array is + // moved into JavaScript. These two fields add 16 bytes to JSCommonJSModule. + // `m_childrenValue` can be set to any value via the user-exposed setter, + // but Bun does not test that behavior besides ensuring it does not crash. + mutable JSC::WriteBarrier m_childrenValue; + // This must be WriteBarrier to compile; always JSCommonJSModule + WTF::Vector> m_children; // Visited by the GC. When the module is assigned a non-JSCommonJSModule // parent, it is assigned to this field. diff --git a/src/bun.js/bindings/ImportMetaObject.cpp b/src/bun.js/bindings/ImportMetaObject.cpp index a115b3162f..31aeec3de9 100644 --- a/src/bun.js/bindings/ImportMetaObject.cpp +++ b/src/bun.js/bindings/ImportMetaObject.cpp @@ -304,7 +304,7 @@ extern "C" JSC::EncodedJSValue functionImportMeta__resolveSyncPrivate(JSC::JSGlo if (LIKELY(globalObject)) { if (UNLIKELY(globalObject->hasOverriddenModuleResolveFilenameFunction)) { auto overrideHandler = jsCast(globalObject->m_moduleResolveFilenameFunction.getInitializedOnMainThread(globalObject)); - if (UNLIKELY(overrideHandler)) { + if (LIKELY(overrideHandler)) { ASSERT(overrideHandler->isCallable()); JSValue parentModuleObject = globalObject->requireMap()->get(globalObject, from); diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 8c3a701fc8..266f101ad4 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -4014,6 +4014,7 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) visitor.append(thisObject->m_currentNapiHandleScopeImpl); thisObject->m_moduleResolveFilenameFunction.visit(visitor); + thisObject->m_moduleRunMainFunction.visit(visitor); thisObject->m_nodeModuleConstructor.visit(visitor); thisObject->m_asyncBoundFunctionStructure.visit(visitor); thisObject->m_bunObject.visit(visitor); diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index 768278f177..7fa6cf7da1 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -398,6 +398,7 @@ public: mutable WriteBarrier m_readableStreamToFormData; LazyProperty m_moduleResolveFilenameFunction; + LazyProperty m_moduleRunMainFunction; LazyProperty m_nodeModuleConstructor; mutable WriteBarrier m_nextTickQueue; @@ -632,6 +633,8 @@ public: bool hasOverriddenModuleResolveFilenameFunction = false; // De-optimization once `require("module").wrapper` or `require("module").wrap` is written to bool hasOverriddenModuleWrapper = false; + // De-optimization once `require("module").runMain` is written to + bool hasOverriddenModuleRunMain = false; WTF::Vector> m_napiEnvs; napi_env makeNapiEnv(const napi_module&); diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 39dfe62399..d0f3a263e7 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -3179,19 +3179,7 @@ JSC__JSModuleLoader__loadAndEvaluateModule(JSC__JSGlobalObject* globalObject, JSC::JSNativeStdFunction* resolverFunction = JSC::JSNativeStdFunction::create( vm, globalObject, 1, String(), resolverFunctionCallback); - auto result = promise->then(globalObject, resolverFunction, nullptr); - - // if (promise->status(globalObject->vm()) == - // JSC::JSPromise::Status::Fulfilled) { - // return reinterpret_cast( - // JSC::JSInternalPromise::resolvedPromise( - // globalObject, - // doLink(globalObject, promise->result(globalObject->vm())) - // ) - // ); - // } - - return result; + return promise->then(globalObject, resolverFunction, nullptr); } #pragma mark - JSC::JSPromise diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 8913273c7d..417fdf8cda 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -829,6 +829,7 @@ pub const VirtualMachine = struct { /// Used by bun:test to set global hooks for beforeAll, beforeEach, etc. is_in_preload: bool = false, + has_patched_run_main: bool = false, transpiler_store: JSC.RuntimeTranspilerStore, @@ -865,7 +866,7 @@ pub const VirtualMachine = struct { rare_data: ?*JSC.RareData = null, is_us_loop_entered: bool = false, - pending_internal_promise: *JSInternalPromise = undefined, + pending_internal_promise: ?*JSInternalPromise = null, entry_point_result: struct { value: JSC.Strong = .empty, cjs_set_value: bool = false, @@ -1270,7 +1271,7 @@ pub const VirtualMachine = struct { } pub fn handlePendingInternalPromiseRejection(this: *JSC.VirtualMachine) void { - var promise = this.pending_internal_promise; + var promise = this.pending_internal_promise.?; if (promise.status(this.global.vm()) == .rejected and !promise.isHandled(this.global.vm())) { _ = this.unhandledRejection(this.global, promise.result(this.global.vm()), promise.asValue()); promise.setHandled(this.global.vm()); @@ -2998,12 +2999,12 @@ pub const VirtualMachine = struct { // pending_internal_promise can change if hot module reloading is enabled if (this.isWatcherEnabled()) { this.eventLoop().performGC(); - switch (this.pending_internal_promise.status(this.global.vm())) { + switch (this.pending_internal_promise.?.status(this.global.vm())) { .pending => { - while (this.pending_internal_promise.status(this.global.vm()) == .pending) { + while (this.pending_internal_promise.?.status(this.global.vm()) == .pending) { this.eventLoop().tick(); - if (this.pending_internal_promise.status(this.global.vm()) == .pending) { + if (this.pending_internal_promise.?.status(this.global.vm()) == .pending) { this.eventLoop().autoTick(); } } @@ -3056,11 +3057,26 @@ pub const VirtualMachine = struct { } if (!this.transpiler.options.disable_transpilation) { - if (try this.loadPreloads()) |promise| { - JSValue.fromCell(promise).ensureStillAlive(); - JSValue.fromCell(promise).protect(); - this.pending_internal_promise = promise; - return promise; + if (this.preload.len > 0) { + if (try this.loadPreloads()) |promise| { + JSValue.fromCell(promise).ensureStillAlive(); + JSValue.fromCell(promise).protect(); + this.pending_internal_promise = promise; + return promise; + } + + // Check if Module.runMain was patched + const prev = this.pending_internal_promise; + if (this.has_patched_run_main) { + @branchHint(.cold); + this.pending_internal_promise = null; + const ret = NodeModuleModule__callOverriddenRunMain(this.global, bun.String.createUTF8ForJS(this.global, main_file_name)); + if (this.pending_internal_promise == prev or this.pending_internal_promise == null) { + this.pending_internal_promise = JSInternalPromise.resolvedPromise(this.global, ret); + return this.pending_internal_promise.?; + } + return (this.pending_internal_promise orelse prev).?; + } } const promise = if (!this.main_is_html_entrypoint) @@ -3080,6 +3096,18 @@ pub const VirtualMachine = struct { } } + extern "C" fn NodeModuleModule__callOverriddenRunMain(global: *JSGlobalObject, argv1: JSValue) JSValue; + export fn Bun__VirtualMachine__setOverrideModuleRunMain(vm: *VirtualMachine, is_patched: bool) void { + if (vm.is_in_preload) { + vm.has_patched_run_main = is_patched; + } + } + export fn Bun__VirtualMachine__setOverrideModuleRunMainPromise(vm: *VirtualMachine, promise: *JSInternalPromise) void { + if (vm.pending_internal_promise == null) { + vm.pending_internal_promise = promise; + } + } + pub fn reloadEntryPointForTestRunner(this: *VirtualMachine, entry_path: []const u8) !*JSInternalPromise { this.has_loaded = false; this.main = entry_path; @@ -3118,7 +3146,7 @@ pub const VirtualMachine = struct { return error.WorkerTerminated; } } - return this.pending_internal_promise; + return this.pending_internal_promise.?; } pub fn loadEntryPointForTestRunner(this: *VirtualMachine, entry_path: string) anyerror!*JSInternalPromise { @@ -3127,12 +3155,12 @@ pub const VirtualMachine = struct { // pending_internal_promise can change if hot module reloading is enabled if (this.isWatcherEnabled()) { this.eventLoop().performGC(); - switch (this.pending_internal_promise.status(this.global.vm())) { + switch (this.pending_internal_promise.?.status(this.global.vm())) { .pending => { - while (this.pending_internal_promise.status(this.global.vm()) == .pending) { + while (this.pending_internal_promise.?.status(this.global.vm()) == .pending) { this.eventLoop().tick(); - if (this.pending_internal_promise.status(this.global.vm()) == .pending) { + if (this.pending_internal_promise.?.status(this.global.vm()) == .pending) { this.eventLoop().autoTick(); } } @@ -3150,7 +3178,7 @@ pub const VirtualMachine = struct { this.eventLoop().autoTick(); - return this.pending_internal_promise; + return this.pending_internal_promise.?; } pub fn loadEntryPoint(this: *VirtualMachine, entry_path: string) anyerror!*JSInternalPromise { @@ -3159,12 +3187,12 @@ pub const VirtualMachine = struct { // pending_internal_promise can change if hot module reloading is enabled if (this.isWatcherEnabled()) { this.eventLoop().performGC(); - switch (this.pending_internal_promise.status(this.global.vm())) { + switch (this.pending_internal_promise.?.status(this.global.vm())) { .pending => { - while (this.pending_internal_promise.status(this.global.vm()) == .pending) { + while (this.pending_internal_promise.?.status(this.global.vm()) == .pending) { this.eventLoop().tick(); - if (this.pending_internal_promise.status(this.global.vm()) == .pending) { + if (this.pending_internal_promise.?.status(this.global.vm()) == .pending) { this.eventLoop().autoTick(); } } @@ -3180,7 +3208,7 @@ pub const VirtualMachine = struct { this.waitForPromise(.{ .internal = promise }); } - return this.pending_internal_promise; + return this.pending_internal_promise.?; } pub fn addListeningSocketForWatchMode(this: *VirtualMachine, socket: bun.FileDescriptor) void { diff --git a/src/bun.js/modules/NodeModuleModule.cpp b/src/bun.js/modules/NodeModuleModule.cpp index 32da70399a..be4a440557 100644 --- a/src/bun.js/modules/NodeModuleModule.cpp +++ b/src/bun.js/modules/NodeModuleModule.cpp @@ -8,6 +8,10 @@ #include #include #include +#include +#include +#include "JavaScriptCore/Completion.h" +#include "JavaScriptCore/JSNativeStdFunction.h" #include "PathInlines.h" #include "ZigGlobalObject.h" @@ -372,7 +376,6 @@ JSC_DEFINE_CUSTOM_GETTER(nodeModuleResolveFilename, EncodedJSValue thisValue, PropertyName propertyName)) { - auto* globalObject = defaultGlobalObject(lexicalGlobalObject); return JSValue::encode( globalObject->m_moduleResolveFilenameFunction.getInitializedOnMainThread( @@ -724,11 +727,76 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionLoad, (JSGlobalObject * globalObject, JSC::Ca return JSC::JSValue::encode(JSC::jsUndefined()); } -JSC_DEFINE_HOST_FUNCTION(jsFunctionRunMain, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +static JSC::EncodedJSValue resolverFunctionCallback(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame) { return JSC::JSValue::encode(JSC::jsUndefined()); } +extern "C" void Bun__VirtualMachine__setOverrideModuleRunMainPromise(void* bunVM, JSInternalPromise* promise); +JSC_DEFINE_HOST_FUNCTION(jsFunctionRunMain, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto arg1 = callFrame->argument(0); + auto name = makeAtomString(arg1.toWTFString(globalObject)); + + auto* promise = JSC::loadAndEvaluateModule(globalObject, name, JSC::jsUndefined(), JSC::jsUndefined()); + RETURN_IF_EXCEPTION(scope, {}); + JSC::JSNativeStdFunction* resolverFunction = JSC::JSNativeStdFunction::create( + vm, globalObject, 1, String(), resolverFunctionCallback); + + auto result = promise->then(globalObject, resolverFunction, nullptr); + Bun__VirtualMachine__setOverrideModuleRunMainPromise(defaultGlobalObject(globalObject)->bunVM(), result); + + return JSC::JSValue::encode(JSC::jsUndefined()); +} + +JSC_DEFINE_CUSTOM_GETTER(moduleRunMain, + (JSGlobalObject * lexicalGlobalObject, + EncodedJSValue thisValue, + PropertyName propertyName)) +{ + auto* globalObject = defaultGlobalObject(lexicalGlobalObject); + + return JSValue::encode( + globalObject->m_moduleRunMainFunction.getInitializedOnMainThread( + globalObject)); +} + +extern "C" void Bun__VirtualMachine__setOverrideModuleRunMain(void* bunVM, bool isOriginal); +extern "C" JSC::EncodedJSValue NodeModuleModule__callOverriddenRunMain(Zig::GlobalObject* global, JSValue argv1) +{ + auto overrideHandler = jsCast(global->m_moduleRunMainFunction.get(global)); + MarkedArgumentBuffer args; + args.append(argv1); + return JSC::JSValue::encode(JSC::profiledCall(global, JSC::ProfilingReason::API, overrideHandler, JSC::getCallData(overrideHandler), global, args)); +} + +JSC_DEFINE_CUSTOM_SETTER(setModuleRunMain, + (JSGlobalObject * lexicalGlobalObject, + EncodedJSValue thisValue, EncodedJSValue encodedValue, + PropertyName propertyName)) +{ + auto* globalObject = defaultGlobalObject(lexicalGlobalObject); + auto value = JSValue::decode(encodedValue); + if (value.isCell()) { + bool isOriginal = false; + if (value.isCallable()) { + JSC::CallData callData = JSC::getCallData(value); + if (callData.type == JSC::CallData::Type::Native) { + if (callData.native.function.untaggedPtr() == &jsFunctionRunMain) { + isOriginal = true; + } + } + } + Bun__VirtualMachine__setOverrideModuleRunMain(globalObject->bunVM(), !isOriginal); + globalObject->m_moduleRunMainFunction.set( + lexicalGlobalObject->vm(), globalObject, value.asCell()); + } + + return true; +} + JSC_DEFINE_HOST_FUNCTION(jsFunctionPreloadModules, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) @@ -785,13 +853,13 @@ builtinModules getBuiltinModulesObject PropertyCallback constants getConstantsObject PropertyCallback createRequire jsFunctionNodeModuleCreateRequire Function 1 enableCompileCache jsFunctionEnableCompileCache Function 0 -findSourceMap jsFunctionFindSourceMap Function 0 +findSourceMap jsFunctionFindSourceMap Function 0 getCompileCacheDir jsFunctionGetCompileCacheDir Function 0 globalPaths getGlobalPathsObject PropertyCallback isBuiltin jsFunctionIsBuiltinModule Function 1 prototype getModulePrototypeObject PropertyCallback register jsFunctionRegister Function 1 -runMain jsFunctionRunMain Function 0 +runMain moduleRunMain CustomAccessor SourceMap getSourceMapFunction PropertyCallback syncBuiltinESMExports jsFunctionSyncBuiltinESMExports Function 0 wrap jsFunctionWrap Function 1 @@ -866,6 +934,15 @@ void addNodeModuleConstructorProperties(JSC::VM& vm, init.set(moduleConstructor); }); + globalObject->m_moduleRunMainFunction.initLater( + [](const Zig::GlobalObject::Initializer& init) { + JSFunction* runMainFunction = JSFunction::create( + init.vm, init.owner, 2, "runMain"_s, + jsFunctionRunMain, JSC::ImplementationVisibility::Public, + JSC::NoIntrinsic, jsFunctionRunMain); + init.set(runMainFunction); + }); + globalObject->m_moduleResolveFilenameFunction.initLater( [](const Zig::GlobalObject::Initializer& init) { JSFunction* resolveFilenameFunction = JSFunction::create( diff --git a/src/cli.zig b/src/cli.zig index 0bd52e3b9e..c2958f4771 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -221,6 +221,7 @@ pub const Arguments = struct { clap.parseParam("--no-clear-screen Disable clearing the terminal screen on reload when --hot or --watch is enabled") catch unreachable, clap.parseParam("--smol Use less memory, but run garbage collection more often") catch unreachable, clap.parseParam("-r, --preload ... Import a module before other modules are loaded") catch unreachable, + clap.parseParam("--require ... Alias of --preload, for Node.js compatibility") catch unreachable, clap.parseParam("--inspect ? Activate Bun's debugger") catch unreachable, clap.parseParam("--inspect-wait ? Activate Bun's debugger, wait for a connection before executing") catch unreachable, clap.parseParam("--inspect-brk ? Activate Bun's debugger, set breakpoint on first line of code and wait") catch unreachable, @@ -399,10 +400,10 @@ pub const Arguments = struct { if (config_path_.len == 0 and (user_config_path_ != null or Command.Tag.always_loads_config.get(cmd) or (cmd == .AutoCommand and - // "bun" - (ctx.positionals.len == 0 or - // "bun file.js" - ctx.positionals.len > 0 and options.defaultLoaders.has(std.fs.path.extension(ctx.positionals[0])))))) + // "bun" + (ctx.positionals.len == 0 or + // "bun file.js" + ctx.positionals.len > 0 and options.defaultLoaders.has(std.fs.path.extension(ctx.positionals[0])))))) { config_path_ = "bunfig.toml"; auto_loaded = true; @@ -672,6 +673,7 @@ pub const Arguments = struct { // runtime commands if (cmd == .AutoCommand or cmd == .RunCommand or cmd == .TestCommand or cmd == .RunAsNodeCommand) { const preloads = args.options("--preload"); + const preloads2 = args.options("--require"); if (args.flag("--hot")) { ctx.debug.hot_reload = .hot; @@ -751,13 +753,23 @@ pub const Arguments = struct { } } - if (ctx.preloads.len > 0 and preloads.len > 0) { - var all = std.ArrayList(string).initCapacity(ctx.allocator, ctx.preloads.len + preloads.len) catch unreachable; + if (ctx.preloads.len > 0 and (preloads.len > 0 or preloads2.len > 0)) { + var all = std.ArrayList(string).initCapacity(ctx.allocator, ctx.preloads.len + preloads.len + preloads2.len) catch unreachable; all.appendSliceAssumeCapacity(ctx.preloads); all.appendSliceAssumeCapacity(preloads); + all.appendSliceAssumeCapacity(preloads2); ctx.preloads = all.items; } else if (preloads.len > 0) { - ctx.preloads = preloads; + if (preloads2.len > 0) { + var all = std.ArrayList(string).initCapacity(ctx.allocator, preloads.len + preloads2.len) catch unreachable; + all.appendSliceAssumeCapacity(preloads); + all.appendSliceAssumeCapacity(preloads2); + ctx.preloads = all.items; + } else { + ctx.preloads = preloads; + } + } else if (preloads2.len > 0) { + ctx.preloads = preloads2; } if (args.option("--print")) |script| { @@ -2134,7 +2146,7 @@ pub const Command = struct { const use_bunx = !HardcodedNonBunXList.has(template_name) and (!strings.containsComptime(template_name, "/") or - strings.startsWithChar(template_name, '@')) and + strings.startsWithChar(template_name, '@')) and example_tag != CreateCommandExample.Tag.local_folder; if (use_bunx) { diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts index d6fc776333..39cd4e50d3 100644 --- a/src/js/builtins.d.ts +++ b/src/js/builtins.d.ts @@ -489,7 +489,7 @@ declare function $createCommonJSModule( declare function $evaluateCommonJSModule( moduleToEvaluate: CommonJSModuleRecord, sourceModule: CommonJSModuleRecord -): void; +): CommonJSModuleRecord[]; declare function $overridableRequire(this: CommonJSModuleRecord, id: string): any; diff --git a/src/js/builtins/Module.ts b/src/js/builtins/Module.ts index e96b2c54ad..6233ce6307 100644 --- a/src/js/builtins/Module.ts +++ b/src/js/builtins/Module.ts @@ -20,7 +20,10 @@ export function overridableRequire(this: CommonJSModuleRecord, originalId: strin // read the require cache. Though they never write to it, which is so silly. const existing = $requireMap.$get(originalId); if (existing) { - $evaluateCommonJSModule(existing, this); + const c = $evaluateCommonJSModule(existing, this); + if (c && c.indexOf(existing) === -1) { + c.push(existing); + } return existing.exports; } } @@ -46,7 +49,10 @@ export function overridableRequire(this: CommonJSModuleRecord, originalId: strin // we evaluate it "early", we'll get an empty object instead of the module // exports. // - $evaluateCommonJSModule(existing, this); + const c = $evaluateCommonJSModule(existing, this); + if (c && c.indexOf(existing) === -1) { + c.push(existing); + } return existing.exports; } } @@ -129,7 +135,10 @@ export function overridableRequire(this: CommonJSModuleRecord, originalId: strin } } - $evaluateCommonJSModule(mod, this); + const c = $evaluateCommonJSModule(mod, this); + if (c && c.indexOf(mod) === -1) { + c.push(mod); + } return mod.exports; } diff --git a/test/bake/client-fixture.mjs b/test/bake/client-fixture.mjs index ce32a350e4..ff3e7d270f 100644 --- a/test/bake/client-fixture.mjs +++ b/test/bake/client-fixture.mjs @@ -24,9 +24,13 @@ let expectingReload = false; let webSockets = []; let pendingReload = null; let pendingReloadTimer = null; -let updatingTimer = null; +let isUpdating = null; function reset() { + if (isUpdating !== null) { + clearImmediate(isUpdating); + isUpdating = null; + } for (const ws of webSockets) { ws.onclose = () => {}; ws.onerror = () => {}; @@ -57,21 +61,6 @@ function createWindow(windowUrl) { height: 768, }); - let hasReadyEventListener = false; - window["bun do not use this outside of internal testing or else i'll cry"] = ({ onEvent }) => { - onEvent("bun:afterUpdate", () => { - setTimeout(() => { - process.send({ type: "received-hmr-event", args: [] }); - }, 50); - }); - hasReadyEventListener = true; - onEvent("bun:ready", () => { - setTimeout(() => { - process.send({ type: "received-hmr-event", args: [] }); - }, 50); - }); - }; - window.fetch = async function (url, options) { if (typeof url === "string") { url = new URL(url, windowUrl).href; @@ -87,14 +76,11 @@ function createWindow(windowUrl) { webSockets.push(this); this.addEventListener("message", event => { const data = new Uint8Array(event.data); - if (data[0] === "e".charCodeAt(0)) { - if (updatingTimer) { - clearTimeout(updatingTimer); - } - updatingTimer = setTimeout(() => { + if (data[0] === "u".charCodeAt(0) || data[0] === "e".charCodeAt(0)) { + isUpdating = setImmediate(() => { process.send({ type: "received-hmr-event", args: [] }); - updatingTimer = null; - }, 250); + isUpdating = null; + }); } if (!allowWebSocketMessages) { const allowedTypes = ["n", "r"]; @@ -134,11 +120,75 @@ function createWindow(windowUrl) { info: (...args) => { if (args[0]?.startsWith("[Bun] Hot-module-reloading socket connected")) { // Wait for all CSS assets to be fully loaded before emitting the event - if (!hasReadyEventListener) { - setTimeout(() => { - process.send({ type: "received-hmr-event", args: [] }); - }, 50); - } + let checkAttempts = 0; + const MAX_CHECK_ATTEMPTS = 20; // Prevent infinite waiting + + const checkCSSLoaded = () => { + checkAttempts++; + + // Get all link elements with rel="stylesheet" + const styleLinks = window.document.querySelectorAll('link[rel="stylesheet"]'); + // Get all style elements + const styleTags = window.document.querySelectorAll("style"); + // Check for adoptedStyleSheets + const adoptedSheets = window.document.adoptedStyleSheets || []; + + // If no stylesheets of any kind, just emit the event + if (styleLinks.length === 0 && styleTags.length === 0 && adoptedSheets.length === 0) { + process.nextTick(() => { + process.send({ type: "received-hmr-event", args: [] }); + }); + return; + } + + // Check if all stylesheets are loaded + let allLoaded = true; + let pendingCount = 0; + + // Check link elements + for (const link of styleLinks) { + // If the stylesheet is not loaded yet + if (!link.sheet) { + allLoaded = false; + pendingCount++; + } + } + + // Check style elements - these should be loaded immediately + for (const style of styleTags) { + if (!style.sheet) { + allLoaded = false; + pendingCount++; + } + } + + // Check adoptedStyleSheets - these should be loaded immediately + for (const sheet of adoptedSheets) { + if (!sheet.cssRules) { + allLoaded = false; + pendingCount++; + } + } + + if (allLoaded || checkAttempts >= MAX_CHECK_ATTEMPTS) { + // All CSS is loaded or we've reached max attempts, emit the event + if (checkAttempts >= MAX_CHECK_ATTEMPTS && !allLoaded) { + console.warn("[W] Reached maximum CSS load check attempts, proceeding anyway"); + } + process.nextTick(() => { + process.send({ type: "received-hmr-event", args: [] }); + }); + } else { + // Wait a bit and check again + console.info( + `[I] Waiting for ${pendingCount} CSS assets to load (attempt ${checkAttempts}/${MAX_CHECK_ATTEMPTS})...`, + ); + setTimeout(checkCSSLoaded, 50); + } + }; + + // Start checking for CSS loaded state + checkCSSLoaded(); } if (args[0]?.startsWith("[WS] receive message")) return; if (args[0]?.startsWith("Updated modules:")) return; @@ -154,10 +204,6 @@ function createWindow(windowUrl) { }; window.location.reload = async () => { - if (updatingTimer) { - clearTimeout(updatingTimer); - } - console.info("[I] location.reload()"); reset(); if (expectingReload) { // Permission already granted, proceed with reload diff --git a/test/cli/run/require-cache.test.ts b/test/cli/run/require-cache.test.ts index a950c27a5a..b3087a9178 100644 --- a/test/cli/run/require-cache.test.ts +++ b/test/cli/run/require-cache.test.ts @@ -46,11 +46,12 @@ describe.skipIf(isBroken && isIntelMacOS)("files transpiled and loaded don't lea "require-cache-bug-leak-fixture.js": ` const path = require.resolve("./index.js"); const gc = global.gc || globalThis?.Bun?.gc || (() => {}); + const noChildren = module.children = { indexOf() { return 0; } }; // disable children tracking function bust() { const mod = require.cache[path]; if (mod) { mod.parent = null; - mod.children = []; + mod.children = noChildren; delete require.cache[path]; } } @@ -72,7 +73,7 @@ describe.skipIf(isBroken && isIntelMacOS)("files transpiled and loaded don't lea console.log("RSS diff", (diff / 1024 / 1024) | 0, "MB"); console.log("RSS", (diff / 1024 / 1024) | 0, "MB"); if (diff > 100 * 1024 * 1024) { - // Bun v1.1.21 reported 844 MB here on macoS arm64. + // Bun v1.1.21 reported 844 MB here on macOS arm64. throw new Error("Memory leak detected"); } diff --git a/test/cli/run/run-crash-handler.test.ts b/test/cli/run/run-crash-handler.test.ts index 7213cee4cc..6803ccf8f7 100644 --- a/test/cli/run/run-crash-handler.test.ts +++ b/test/cli/run/run-crash-handler.test.ts @@ -36,7 +36,7 @@ test("raise ignoring panic handler does not trigger the panic handler", async () await proc.exited; - /// Wait two seconds for a slow http request, or continue immediatly once the request is heard. + /// Wait two seconds for a slow http request, or continue immediately once the request is heard. await Promise.race([resolve_handler.promise, Bun.sleep(2000)]); expect(proc.exited).resolves.not.toBe(0); diff --git a/test/internal/ban-words.test.ts b/test/internal/ban-words.test.ts index f3f173e781..7d388efcd2 100644 --- a/test/internal/ban-words.test.ts +++ b/test/internal/ban-words.test.ts @@ -27,7 +27,7 @@ const words: Record "alloc.ptr !=": { reason: "The std.mem.Allocator context pointer can be undefined, which makes this comparison undefined behavior" }, "== alloc.ptr": { reason: "The std.mem.Allocator context pointer can be undefined, which makes this comparison undefined behavior" }, "!= alloc.ptr": { reason: "The std.mem.Allocator context pointer can be undefined, which makes this comparison undefined behavior" }, - [String.raw`: [a-zA-Z0-9_\.\*\?\[\]\(\)]+ = undefined,`]: { reason: "Do not default a struct field to undefined", limit: 249, regex: true }, + [String.raw`: [a-zA-Z0-9_\.\*\?\[\]\(\)]+ = undefined,`]: { reason: "Do not default a struct field to undefined", limit: 248, regex: true }, "usingnamespace": { reason: "This brings Bun away from incremental / faster compile times.", limit: 496 }, }; const words_keys = [...Object.keys(words)]; diff --git a/test/js/node/module/children-fixture/a.cjs b/test/js/node/module/children-fixture/a.cjs new file mode 100644 index 0000000000..c8c1052019 --- /dev/null +++ b/test/js/node/module/children-fixture/a.cjs @@ -0,0 +1,30 @@ +require("./b.cjs"); +require("./d.cjs"); +require("./b.cjs"); +if (process.argv.includes("--access-early")) { + module.children; +} +require("./b.cjs"); +require("./b.cjs"); +require("./f.cjs"); +require("./g.cjs"); + +let seen = new Set(); +function iter(module, indent = 0) { + if (require.cache[module.filename] !== module) { + throw new Error("module.filename is not the same as require.cache[module.filename]"); + } + let isSeen = seen.has(module); + console.log( + `${" ".repeat(indent)}${module.id === module.filename ? module.id : `${module.id} (${module.filename})`}${isSeen ? " (seen)" : ""}` + .replaceAll(__dirname, ".") + .replaceAll("\\", "/"), + ); + seen.add(module); + if (isSeen) return; + for (let child of module.children) { + iter(child, indent + 1); + } +} + +iter(module); diff --git a/test/js/node/module/children-fixture/b.cjs b/test/js/node/module/children-fixture/b.cjs new file mode 100644 index 0000000000..eb9c3935f8 --- /dev/null +++ b/test/js/node/module/children-fixture/b.cjs @@ -0,0 +1,3 @@ +require("./a.cjs"); +require("./b.cjs"); +require("./c.cjs"); diff --git a/test/js/node/module/children-fixture/c.cjs b/test/js/node/module/children-fixture/c.cjs new file mode 100644 index 0000000000..49a694e3e9 --- /dev/null +++ b/test/js/node/module/children-fixture/c.cjs @@ -0,0 +1 @@ +require("./d.cjs"); diff --git a/test/js/node/module/children-fixture/d.cjs b/test/js/node/module/children-fixture/d.cjs new file mode 100644 index 0000000000..49a694e3e9 --- /dev/null +++ b/test/js/node/module/children-fixture/d.cjs @@ -0,0 +1 @@ +require("./d.cjs"); diff --git a/test/js/node/module/children-fixture/f.cjs b/test/js/node/module/children-fixture/f.cjs new file mode 100644 index 0000000000..2e3162a71b --- /dev/null +++ b/test/js/node/module/children-fixture/f.cjs @@ -0,0 +1,4 @@ +if (process.argv.includes("--access-early")) { + module.children; +} +require("./d.cjs"); diff --git a/test/js/node/module/children-fixture/g.cjs b/test/js/node/module/children-fixture/g.cjs new file mode 100644 index 0000000000..24fae97b86 --- /dev/null +++ b/test/js/node/module/children-fixture/g.cjs @@ -0,0 +1,3 @@ +require("./b.cjs"); +require("./a.cjs"); +require("./h.cjs"); diff --git a/test/js/node/module/children-fixture/h.cjs b/test/js/node/module/children-fixture/h.cjs new file mode 100644 index 0000000000..934b8b369a --- /dev/null +++ b/test/js/node/module/children-fixture/h.cjs @@ -0,0 +1,3 @@ +require("./i.cjs"); +require("./j.cjs"); +require("./k.cjs"); diff --git a/test/js/node/module/children-fixture/i.cjs b/test/js/node/module/children-fixture/i.cjs new file mode 100644 index 0000000000..73b68217f0 --- /dev/null +++ b/test/js/node/module/children-fixture/i.cjs @@ -0,0 +1 @@ +require("./j.cjs"); diff --git a/test/js/node/module/children-fixture/j.cjs b/test/js/node/module/children-fixture/j.cjs new file mode 100644 index 0000000000..934b8b369a --- /dev/null +++ b/test/js/node/module/children-fixture/j.cjs @@ -0,0 +1,3 @@ +require("./i.cjs"); +require("./j.cjs"); +require("./k.cjs"); diff --git a/test/js/node/module/children-fixture/k.cjs b/test/js/node/module/children-fixture/k.cjs new file mode 100644 index 0000000000..73b68217f0 --- /dev/null +++ b/test/js/node/module/children-fixture/k.cjs @@ -0,0 +1 @@ +require("./j.cjs"); diff --git a/test/js/node/module/node-module-module.test.js b/test/js/node/module/node-module-module.test.js index cccc70e9cb..2aadb745da 100644 --- a/test/js/node/module/node-module-module.test.js +++ b/test/js/node/module/node-module-module.test.js @@ -178,3 +178,56 @@ test("require cache node builtins specifier", () => { test("require a cjs file uses the 'module.exports' export", () => { expect(require("./esm_to_cjs_interop.mjs")).toEqual(Symbol.for("meow")); }); + +test("Module.runMain", () => { + const { stdout, exitCode } = Bun.spawnSync({ + cmd: [bunExe(), "--require", path.join(import.meta.dir, "overwrite-module-run-main-1.cjs"), path.join(import.meta.dir, "overwrite-module-run-main-2.cjs")], + env: bunEnv, + stderr: "inherit", + }); + + expect(stdout.toString().trim()).toBe("pass"); + expect(exitCode).toBe(0); +}); +test("Module.runMain 2", () => { + const { stdout, exitCode } = Bun.spawnSync({ + cmd: [bunExe(), "--require", path.join(import.meta.dir, "overwrite-module-run-main-3.cjs"), path.join(import.meta.dir, "overwrite-module-run-main-2.cjs")], + env: bunEnv, + stderr: "inherit", + }); + + expect(stdout.toString().trim()).toBe("pass"); + expect(exitCode).toBe(0); +}); +test.each([ + "no args", + "--access-early", +])("children, %s", arg => { + const { stdout, exitCode } = Bun.spawnSync({ + cmd: [bunExe(), path.join(import.meta.dir, "children-fixture/a.cjs"), arg], + env: bunEnv, + stderr: "inherit", + }); + expect(stdout.toString().trim()).toBe(`. (./a.cjs) + ./b.cjs + . (./a.cjs) (seen) + ./b.cjs (seen) + ./c.cjs + ./d.cjs + ./d.cjs (seen) + ./d.cjs (seen) + ./f.cjs + ./d.cjs (seen) + ./g.cjs + ./b.cjs (seen) + . (./a.cjs) (seen) + ./h.cjs + ./i.cjs + ./j.cjs + ./i.cjs (seen) + ./j.cjs (seen) + ./k.cjs + ./j.cjs (seen) + ./j.cjs (seen) + ./k.cjs (seen)`); +}); \ No newline at end of file diff --git a/test/js/node/module/overwrite-module-run-main-1.cjs b/test/js/node/module/overwrite-module-run-main-1.cjs new file mode 100644 index 0000000000..29c462aa4d --- /dev/null +++ b/test/js/node/module/overwrite-module-run-main-1.cjs @@ -0,0 +1,6 @@ +const Module = require("module"); +const old = Module.runMain; +Module.runMain = (...args) => { + process.stdout.write("pa"); + return old(...args); +}; diff --git a/test/js/node/module/overwrite-module-run-main-2.cjs b/test/js/node/module/overwrite-module-run-main-2.cjs new file mode 100644 index 0000000000..faaf641828 --- /dev/null +++ b/test/js/node/module/overwrite-module-run-main-2.cjs @@ -0,0 +1 @@ +console.log("ss"); diff --git a/test/js/node/module/overwrite-module-run-main-3.cjs b/test/js/node/module/overwrite-module-run-main-3.cjs new file mode 100644 index 0000000000..4298ee2c29 --- /dev/null +++ b/test/js/node/module/overwrite-module-run-main-3.cjs @@ -0,0 +1,5 @@ +const Module = require("module"); +const old = Module.runMain; +Module.runMain = (...args) => { + process.stdout.write("pass"); +}; diff --git a/test/js/node/test/.gitignore b/test/js/node/test/.gitignore index 131056112f..d758bbb62c 100644 --- a/test/js/node/test/.gitignore +++ b/test/js/node/test/.gitignore @@ -6,7 +6,4 @@ fixtures/source-map fixtures/snapshot fixtures/repl* .tmp.* -*shadow-realm* -!test-shadow-realm-prepare-stack-trace.js -!test-shadow-realm.js **/fails.txt diff --git a/test/js/node/test/fixtures/es-module-shadow-realm/custom-loaders.js b/test/js/node/test/fixtures/es-module-shadow-realm/custom-loaders.js new file mode 100644 index 0000000000..bf4402250c --- /dev/null +++ b/test/js/node/test/fixtures/es-module-shadow-realm/custom-loaders.js @@ -0,0 +1,15 @@ +// This fixture is used to test that custom loaders are not enabled in the ShadowRealm. + +'use strict'; +const assert = require('assert'); + +async function workInChildProcess() { + // Assert that the process is running with a custom loader. + const moduleNamespace = await import('file:///42.mjs'); + assert.strictEqual(moduleNamespace.default, 42); + + const realm = new ShadowRealm(); + await assert.rejects(realm.importValue('file:///42.mjs', 'default'), TypeError); +} + +workInChildProcess(); diff --git a/test/js/node/test/fixtures/es-module-shadow-realm/preload-main.js b/test/js/node/test/fixtures/es-module-shadow-realm/preload-main.js new file mode 100644 index 0000000000..4258b012ad --- /dev/null +++ b/test/js/node/test/fixtures/es-module-shadow-realm/preload-main.js @@ -0,0 +1,9 @@ +// This fixture is used to test that --require preload modules are not enabled in the ShadowRealm. + +'use strict'; +const assert = require('assert'); + +assert.strictEqual(globalThis.preload, 42); +const realm = new ShadowRealm(); +const value = realm.evaluate(`globalThis.preload`); +assert.strictEqual(value, undefined); diff --git a/test/js/node/test/fixtures/es-module-shadow-realm/preload.js b/test/js/node/test/fixtures/es-module-shadow-realm/preload.js new file mode 100644 index 0000000000..dbbcb65e5a --- /dev/null +++ b/test/js/node/test/fixtures/es-module-shadow-realm/preload.js @@ -0,0 +1 @@ +globalThis.preload = 42; diff --git a/test/js/node/test/fixtures/es-module-shadow-realm/re-export-state-counter.mjs b/test/js/node/test/fixtures/es-module-shadow-realm/re-export-state-counter.mjs new file mode 100644 index 0000000000..50a6aa3fe1 --- /dev/null +++ b/test/js/node/test/fixtures/es-module-shadow-realm/re-export-state-counter.mjs @@ -0,0 +1,3 @@ +// This module verifies that the module specifier is resolved relative to the +// current module and not the current working directory in the ShadowRealm. +export { getCounter } from "./state-counter.mjs"; diff --git a/test/js/node/test/fixtures/es-module-shadow-realm/state-counter.mjs b/test/js/node/test/fixtures/es-module-shadow-realm/state-counter.mjs new file mode 100644 index 0000000000..c547658bf4 --- /dev/null +++ b/test/js/node/test/fixtures/es-module-shadow-realm/state-counter.mjs @@ -0,0 +1,4 @@ +let counter = 0; +export const getCounter = () => { + return counter++; +}; diff --git a/test/js/node/test/fixtures/throws_error.js b/test/js/node/test/fixtures/throws_error.js index b38000894b..a248ea1b74 100644 --- a/test/js/node/test/fixtures/throws_error.js +++ b/test/js/node/test/fixtures/throws_error.js @@ -19,4 +19,5 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. +module.exports = {}; throw new Error('blah'); diff --git a/test/js/node/test/parallel/test-module-children.js b/test/js/node/test/parallel/test-module-children.js new file mode 100644 index 0000000000..e2d3484efb --- /dev/null +++ b/test/js/node/test/parallel/test-module-children.js @@ -0,0 +1,14 @@ +// Flags: --no-deprecation +'use strict'; +require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const path = require('path'); + +const dir = fixtures.path('GH-7131'); +const b = require(path.join(dir, 'b')); +const a = require(path.join(dir, 'a')); + +assert.strictEqual(a.length, 1); +assert.strictEqual(b.length, 0); +assert.deepStrictEqual(a[0].exports, b); diff --git a/test/js/node/test/parallel/test-module-run-main-monkey-patch.js b/test/js/node/test/parallel/test-module-run-main-monkey-patch.js new file mode 100644 index 0000000000..c9f189abb6 --- /dev/null +++ b/test/js/node/test/parallel/test-module-run-main-monkey-patch.js @@ -0,0 +1,18 @@ +'use strict'; + +// This tests that module.runMain can be monkey patched using --require. +// TODO(joyeecheung): This probably should be deprecated. + +require('../common'); +const { path } = require('../common/fixtures'); +const assert = require('assert'); +const { spawnSync } = require('child_process'); + +const child = spawnSync(process.execPath, [ + '--require', + path('monkey-patch-run-main.js'), + path('semicolon.js'), +]); + +assert.strictEqual(child.status, 0); +assert(child.stdout.toString().includes('runMain is monkey patched!')); diff --git a/test/js/node/test/parallel/test-shadow-realm-gc-module.js b/test/js/node/test/parallel/test-shadow-realm-gc-module.js new file mode 100644 index 0000000000..6077bf0314 --- /dev/null +++ b/test/js/node/test/parallel/test-shadow-realm-gc-module.js @@ -0,0 +1,18 @@ +// Flags: --experimental-shadow-realm --max-old-space-size=20 +'use strict'; + +/** + * Verifying modules imported by ShadowRealm instances can be correctly + * garbage collected. + */ + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const { runAndBreathe } = require('../common/gc'); + +const mod = fixtures.fileURL('es-module-shadow-realm', 'state-counter.mjs'); + +runAndBreathe(async () => { + const realm = new ShadowRealm(); + await realm.importValue(mod, 'getCounter'); +}, 100).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-shadow-realm-module.js b/test/js/node/test/parallel/test-shadow-realm-module.js new file mode 100644 index 0000000000..bc0c2c04f6 --- /dev/null +++ b/test/js/node/test/parallel/test-shadow-realm-module.js @@ -0,0 +1,29 @@ +// Flags: --experimental-shadow-realm +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); + +async function main() { + const realm = new ShadowRealm(); + const mod = fixtures.fileURL('es-module-shadow-realm', 'state-counter.mjs'); + const getCounter = await realm.importValue(mod, 'getCounter'); + assert.strictEqual(getCounter(), 0); + const getCounter1 = await realm.importValue(mod, 'getCounter'); + // Returned value is a newly wrapped function. + assert.notStrictEqual(getCounter, getCounter1); + // Verify that the module state is shared between two `importValue` calls. + assert.strictEqual(getCounter1(), 1); + assert.strictEqual(getCounter(), 2); + + const { getCounter: getCounterThisRealm } = await import(mod); + assert.notStrictEqual(getCounterThisRealm, getCounter); + // Verify that the module state is not shared between two realms. + assert.strictEqual(getCounterThisRealm(), 0); + assert.strictEqual(getCounter(), 3); + + // Verify that shadow realm rejects to import a non-existing module. + await assert.rejects(realm.importValue('non-exists', 'exports'), TypeError); +} + +main().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-shadow-realm-preload-module.js b/test/js/node/test/parallel/test-shadow-realm-preload-module.js new file mode 100644 index 0000000000..ebd29c1c4a --- /dev/null +++ b/test/js/node/test/parallel/test-shadow-realm-preload-module.js @@ -0,0 +1,20 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const { spawnSyncAndExitWithoutError } = require('../common/child_process'); + +const commonArgs = [ + '--experimental-shadow-realm', +]; + +async function main() { + // Verifies that --require preload modules are not enabled in the ShadowRealm. + spawnSyncAndExitWithoutError(process.execPath, [ + ...commonArgs, + '--require', + fixtures.path('es-module-shadow-realm', 'preload.js'), + fixtures.path('es-module-shadow-realm', 'preload-main.js'), + ]); +} + +main().then(common.mustCall());