module.children and Module.runMain (#18343)

Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: 190n <ben@bun.sh>
This commit is contained in:
chloe caruso
2025-03-21 16:57:10 -07:00
committed by GitHub
parent f36d480919
commit ddd87fef12
42 changed files with 580 additions and 98 deletions

View File

@@ -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)

View File

@@ -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<Unknown>());
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<JSCommonJSModule*>(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<JSCommonJSModule*>(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<Unknown> childBarrier : mod->m_children) {
JSCommonJSModule* child = jsCast<JSCommonJSModule*>(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<ArrayAllocationProfile*>(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<JSCommonJSModule*>(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<JSCommonJSModule*>(callframe->thisValue());
@@ -596,7 +656,7 @@ JSC_DEFINE_HOST_FUNCTION(functionCommonJSModuleRecord_compile, (JSGlobalObject *
static const struct HashTableValue JSCommonJSModulePrototypeTableValues[] = {
{ "_compile"_s, static_cast<unsigned>(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, functionCommonJSModuleRecord_compile, 2 } },
{ "children"_s, static_cast<unsigned>(PropertyAttribute::PropertyCallback), NoIntrinsic, { HashTableValue::LazyPropertyType, createChildren } },
{ "children"_s, static_cast<unsigned>(PropertyAttribute::CustomAccessor | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, getterChildren, setterChildren } },
{ "filename"_s, static_cast<unsigned>(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, getterFilename, setterFilename } },
{ "id"_s, static_cast<unsigned>(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, getterId, setterId } },
{ "loaded"_s, static_cast<unsigned>(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);

View File

@@ -44,6 +44,17 @@ public:
mutable JSC::WriteBarrier<JSString> m_dirname;
// Initialized lazily; can be overridden.
mutable JSC::WriteBarrier<Unknown> 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<Unknown> m_childrenValue;
// This must be WriteBarrier<Unknown> to compile; always JSCommonJSModule
WTF::Vector<WriteBarrier<Unknown>> m_children;
// Visited by the GC. When the module is assigned a non-JSCommonJSModule
// parent, it is assigned to this field.

View File

@@ -304,7 +304,7 @@ extern "C" JSC::EncodedJSValue functionImportMeta__resolveSyncPrivate(JSC::JSGlo
if (LIKELY(globalObject)) {
if (UNLIKELY(globalObject->hasOverriddenModuleResolveFilenameFunction)) {
auto overrideHandler = jsCast<JSObject*>(globalObject->m_moduleResolveFilenameFunction.getInitializedOnMainThread(globalObject));
if (UNLIKELY(overrideHandler)) {
if (LIKELY(overrideHandler)) {
ASSERT(overrideHandler->isCallable());
JSValue parentModuleObject = globalObject->requireMap()->get(globalObject, from);

View File

@@ -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);

View File

@@ -398,6 +398,7 @@ public:
mutable WriteBarrier<JSFunction> m_readableStreamToFormData;
LazyProperty<JSGlobalObject, JSCell> m_moduleResolveFilenameFunction;
LazyProperty<JSGlobalObject, JSCell> m_moduleRunMainFunction;
LazyProperty<JSGlobalObject, JSObject> m_nodeModuleConstructor;
mutable WriteBarrier<Unknown> 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<std::unique_ptr<napi_env__>> m_napiEnvs;
napi_env makeNapiEnv(const napi_module&);

View File

@@ -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*>(
// JSC::JSInternalPromise::resolvedPromise(
// globalObject,
// doLink(globalObject, promise->result(globalObject->vm()))
// )
// );
// }
return result;
return promise->then(globalObject, resolverFunction, nullptr);
}
#pragma mark - JSC::JSPromise

View File

@@ -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 {

View File

@@ -8,6 +8,10 @@
#include <JavaScriptCore/FunctionPrototype.h>
#include <JavaScriptCore/LazyPropertyInlines.h>
#include <JavaScriptCore/VMTrapsInlines.h>
#include <JavaScriptCore/CallData.h>
#include <JavaScriptCore/JSInternalPromise.h>
#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<JSObject*>(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<JSCell>& 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<JSCell>& init) {
JSFunction* resolveFilenameFunction = JSFunction::create(

View File

@@ -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 <STR>... Import a module before other modules are loaded") catch unreachable,
clap.parseParam("--require <STR>... Alias of --preload, for Node.js compatibility") catch unreachable,
clap.parseParam("--inspect <STR>? Activate Bun's debugger") catch unreachable,
clap.parseParam("--inspect-wait <STR>? Activate Bun's debugger, wait for a connection before executing") catch unreachable,
clap.parseParam("--inspect-brk <STR>? 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) {

View File

@@ -489,7 +489,7 @@ declare function $createCommonJSModule(
declare function $evaluateCommonJSModule(
moduleToEvaluate: CommonJSModuleRecord,
sourceModule: CommonJSModuleRecord
): void;
): CommonJSModuleRecord[];
declare function $overridableRequire(this: CommonJSModuleRecord, id: string): any;

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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");
}

View File

@@ -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);

View File

@@ -27,7 +27,7 @@ const words: Record<string, { reason: string; limit?: number; regex?: boolean }>
"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)];

View File

@@ -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);

View File

@@ -0,0 +1,3 @@
require("./a.cjs");
require("./b.cjs");
require("./c.cjs");

View File

@@ -0,0 +1 @@
require("./d.cjs");

View File

@@ -0,0 +1 @@
require("./d.cjs");

View File

@@ -0,0 +1,4 @@
if (process.argv.includes("--access-early")) {
module.children;
}
require("./d.cjs");

View File

@@ -0,0 +1,3 @@
require("./b.cjs");
require("./a.cjs");
require("./h.cjs");

View File

@@ -0,0 +1,3 @@
require("./i.cjs");
require("./j.cjs");
require("./k.cjs");

View File

@@ -0,0 +1 @@
require("./j.cjs");

View File

@@ -0,0 +1,3 @@
require("./i.cjs");
require("./j.cjs");
require("./k.cjs");

View File

@@ -0,0 +1 @@
require("./j.cjs");

View File

@@ -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)`);
});

View File

@@ -0,0 +1,6 @@
const Module = require("module");
const old = Module.runMain;
Module.runMain = (...args) => {
process.stdout.write("pa");
return old(...args);
};

View File

@@ -0,0 +1 @@
console.log("ss");

View File

@@ -0,0 +1,5 @@
const Module = require("module");
const old = Module.runMain;
Module.runMain = (...args) => {
process.stdout.write("pass");
};

View File

@@ -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

View File

@@ -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();

View File

@@ -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);

View File

@@ -0,0 +1 @@
globalThis.preload = 42;

View File

@@ -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";

View File

@@ -0,0 +1,4 @@
let counter = 0;
export const getCounter = () => {
return counter++;
};

View File

@@ -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');

View File

@@ -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);

View File

@@ -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!'));

View File

@@ -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());

View File

@@ -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());

View File

@@ -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());