diff --git a/src/bun.js/bindings/BunString.cpp b/src/bun.js/bindings/BunString.cpp index 84a7ae8688..f2befc1d43 100644 --- a/src/bun.js/bindings/BunString.cpp +++ b/src/bun.js/bindings/BunString.cpp @@ -27,6 +27,7 @@ #include "JSDOMOperation.h" #include "GCDefferalContext.h" +#include "wtf/text/StringImpl.h" extern "C" void mi_free(void* ptr); @@ -559,4 +560,9 @@ extern "C" bool WTFStringImpl__isThreadSafe( // return wtf->characters8() == reinterpret_cast_ptr(reinterpret_cast(wtf) + tailOffset()); // return wtf->characters16() == reinterpret_cast_ptr(reinterpret_cast(wtf) + tailOffset()); +} + +extern "C" void Bun__WTFStringImpl__ensureHash(WTF::StringImpl* str) +{ + str->hash(); } \ No newline at end of file diff --git a/src/bun.js/bindings/CommonJSModuleRecord.cpp b/src/bun.js/bindings/CommonJSModuleRecord.cpp index 56501f14e7..c029f2cb32 100644 --- a/src/bun.js/bindings/CommonJSModuleRecord.cpp +++ b/src/bun.js/bindings/CommonJSModuleRecord.cpp @@ -30,7 +30,10 @@ */ #include "headers.h" +#include "JavaScriptCore/JSCast.h" +#include #include "root.h" +#include "JavaScriptCore/SourceCode.h" #include "headers-handwritten.h" #include "ZigGlobalObject.h" #include @@ -67,6 +70,8 @@ #include #include #include "PathInlines.h" +#include "wtf/NakedPtr.h" +#include "wtf/URL.h" extern "C" bool Bun__isBunMain(JSC::JSGlobalObject* global, const BunString*); @@ -97,10 +102,10 @@ extern "C" void Bun__VM__setEntryPointEvalResultCJS(void*, EncodedJSValue); static bool evaluateCommonJSModuleOnce(JSC::VM& vm, Zig::GlobalObject* globalObject, JSCommonJSModule* moduleObject, JSString* dirname, JSValue filename, WTF::NakedPtr& exception) { - JSSourceCode* code = moduleObject->sourceCode.get(); + SourceCode code = std::move(moduleObject->sourceCode); // If an exception occurred somewhere else, we might have cleared the source code. - if (UNLIKELY(code == nullptr)) { + if (UNLIKELY(code.isNull())) { auto throwScope = DECLARE_THROW_SCOPE(vm); throwException(globalObject, throwScope, createError(globalObject, "Failed to evaluate module"_s)); exception = throwScope.exception(); @@ -132,25 +137,22 @@ static bool evaluateCommonJSModuleOnce(JSC::VM& vm, Zig::GlobalObject* globalObj globalObject->putDirect(vm, Identifier::fromLatin1(vm, "__filename"_s), filename, 0); globalObject->putDirect(vm, Identifier::fromLatin1(vm, "__dirname"_s), dirname, 0); - JSValue result = JSC::evaluate(globalObject, code->sourceCode(), jsUndefined(), exception); + JSValue result = JSC::evaluate(globalObject, code, jsUndefined(), exception); if (UNLIKELY(exception.get() || result.isEmpty())) { - moduleObject->sourceCode.clear(); return false; } Bun__VM__setEntryPointEvalResultCJS(globalObject->bunVM(), JSValue::encode(result)); - moduleObject->sourceCode.clear(); - return true; } // This will return 0 if there was a syntax error or an allocation failure - JSValue fnValue = JSC::evaluate(globalObject, code->sourceCode(), jsUndefined(), exception); + JSValue fnValue = JSC::evaluate(globalObject, code, jsUndefined(), exception); if (UNLIKELY(exception.get() || fnValue.isEmpty())) { - moduleObject->sourceCode.clear(); + return false; } @@ -171,39 +173,47 @@ static bool evaluateCommonJSModuleOnce(JSC::VM& vm, Zig::GlobalObject* globalObj JSC::call(globalObject, fn, callData, moduleObject, args, exception); - moduleObject->sourceCode.clear(); - return exception.get() == nullptr; } -JSC_DEFINE_HOST_FUNCTION(jsFunctionLoadModule, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe)) +bool JSCommonJSModule::load(JSC::VM& vm, Zig::GlobalObject* globalObject, WTF::NakedPtr& exception) { - auto* globalObject = jsCast(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(globalObject->vm()); - JSCommonJSModule* moduleObject = jsDynamicCast(callframe->argument(0)); - if (!moduleObject) { - RELEASE_AND_RETURN(throwScope, JSValue::encode(jsBoolean(true))); + if (this->hasEvaluated || this->sourceCode.isNull()) { + return true; } - if (moduleObject->hasEvaluated || !moduleObject->sourceCode) { - RELEASE_AND_RETURN(throwScope, JSValue::encode(jsBoolean(true))); - } - - WTF::NakedPtr exception; - evaluateCommonJSModuleOnce( globalObject->vm(), jsCast(globalObject), - moduleObject, - moduleObject->m_dirname.get(), - moduleObject->m_filename.get(), + this, + this->m_dirname.get(), + this->m_filename.get(), exception); if (exception.get()) { // On error, remove the module from the require map/ // so that it can be re-evaluated on the next require. - globalObject->requireMap()->remove(globalObject, moduleObject->id()); + globalObject->requireMap()->remove(globalObject, this->id()); + return false; + } + + return true; +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionLoadModule, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe)) +{ + auto& vm = lexicalGlobalObject->vm(); + auto* globalObject = jsCast(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + JSCommonJSModule* moduleObject = jsDynamicCast(callframe->argument(0)); + if (!moduleObject) { + RELEASE_AND_RETURN(throwScope, JSValue::encode(jsBoolean(true))); + } + + WTF::NakedPtr exception; + + if (!moduleObject->load(vm, globalObject, exception)) { throwException(globalObject, throwScope, exception.get()); exception.clear(); return JSValue::encode({}); @@ -487,15 +497,13 @@ JSC_DEFINE_HOST_FUNCTION(functionCommonJSModuleRecord_compile, (JSGlobalObject * sourceString, "\n})"_s); - SourceCode sourceCode = makeSource( + moduleObject->sourceCode = makeSource( WTFMove(wrappedString), SourceOrigin(URL::fileURLWithFileSystemPath(filenameString)), JSC::SourceTaintedOrigin::Untainted, filenameString, WTF::TextPosition(), JSC::SourceProviderSourceType::Program); - JSSourceCode* jsSourceCode = JSSourceCode::create(vm, WTFMove(sourceCode)); - moduleObject->sourceCode.set(vm, moduleObject, jsSourceCode); auto index = filenameString.reverseFind(PLATFORM_SEP, filenameString.length()); // filenameString is coming from js, any separator could be used @@ -593,15 +601,14 @@ public: const JSC::ClassInfo JSCommonJSModulePrototype::s_info = { "ModulePrototype"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSCommonJSModulePrototype) }; -void JSCommonJSModule::finishCreation(JSC::VM& vm, JSC::JSString* id, JSValue filename, JSC::JSString* dirname, JSC::JSSourceCode* sourceCode) +void JSCommonJSModule::finishCreation(JSC::VM& vm, JSC::JSString* id, JSValue filename, JSC::JSString* dirname, const JSC::SourceCode& sourceCode) { Base::finishCreation(vm); ASSERT(inherits(info())); m_id.set(vm, this, id); m_filename.set(vm, this, filename); m_dirname.set(vm, this, dirname); - if (sourceCode) - this->sourceCode.set(vm, this, sourceCode); + this->sourceCode = sourceCode; } JSC::Structure* JSCommonJSModule::createStructure( @@ -622,7 +629,7 @@ JSCommonJSModule* JSCommonJSModule::create( JSC::JSString* id, JSValue filename, JSC::JSString* dirname, - JSC::JSSourceCode* sourceCode) + const JSC::SourceCode& sourceCode) { JSCommonJSModule* cell = new (NotNull, JSC::allocateCell(vm)) JSCommonJSModule(vm, structure); cell->finishCreation(vm, id, filename, dirname, sourceCode); @@ -664,7 +671,7 @@ JSCommonJSModule* JSCommonJSModule::create( auto* out = JSCommonJSModule::create( vm, globalObject->CommonJSModuleObjectStructure(), - requireMapKey, requireMapKey, dirname, nullptr); + requireMapKey, requireMapKey, dirname, SourceCode()); out->putDirect( vm, @@ -898,7 +905,6 @@ void JSCommonJSModule::visitChildrenImpl(JSCell* cell, Visitor& visitor) ASSERT_GC_OBJECT_INHERITS(thisObject, info()); Base::visitChildren(thisObject, visitor); visitor.append(thisObject->m_id); - visitor.append(thisObject->sourceCode); visitor.append(thisObject->m_filename); visitor.append(thisObject->m_dirname); visitor.append(thisObject->m_paths); @@ -996,19 +1002,16 @@ void RequireResolveFunctionPrototype::finishCreation(JSC::VM& vm) bool JSCommonJSModule::evaluate( Zig::GlobalObject* globalObject, const WTF::String& key, - ResolvedSource source, + ResolvedSource& source, bool isBuiltIn) { auto& vm = globalObject->vm(); auto sourceProvider = Zig::SourceProvider::create(jsCast(globalObject), source, JSC::SourceProviderSourceType::Program, isBuiltIn); this->ignoreESModuleAnnotation = source.tag == ResolvedSourceTagPackageJSONTypeModule; - JSC::SourceCode rawInputSource( - WTFMove(sourceProvider)); - if (this->hasEvaluated) return true; - this->sourceCode.set(vm, this, JSC::JSSourceCode::create(vm, WTFMove(rawInputSource))); + this->sourceCode = WTFMove(JSC::SourceCode(WTFMove(sourceProvider))); WTF::NakedPtr exception; @@ -1031,18 +1034,16 @@ bool JSCommonJSModule::evaluate( std::optional createCommonJSModule( Zig::GlobalObject* globalObject, - ResolvedSource source, + JSValue specifierValue, + ResolvedSource& source, bool isBuiltIn) { JSCommonJSModule* moduleObject = nullptr; WTF::String sourceURL = source.source_url.toWTFString(); - JSValue specifierValue = Bun::toJS(globalObject, source.specifier); JSValue entry = globalObject->requireMap()->get(globalObject, specifierValue); - - auto sourceProvider = Zig::SourceProvider::create(jsCast(globalObject), source, JSC::SourceProviderSourceType::Program, isBuiltIn); bool ignoreESModuleAnnotation = source.tag == ResolvedSourceTagPackageJSONTypeModule; - SourceOrigin sourceOrigin = sourceProvider->sourceOrigin(); + SourceOrigin sourceOrigin; if (entry) { moduleObject = jsDynamicCast(entry); @@ -1050,7 +1051,7 @@ std::optional createCommonJSModule( if (!moduleObject) { auto& vm = globalObject->vm(); - auto* requireMapKey = jsStringWithCache(vm, sourceURL); + auto* requireMapKey = specifierValue.toString(globalObject); auto index = sourceURL.reverseFind(PLATFORM_SEP, sourceURL.length()); JSString* dirname; JSString* filename = requireMapKey; @@ -1060,16 +1061,20 @@ std::optional createCommonJSModule( dirname = jsEmptyString(vm); } + auto sourceProvider = Zig::SourceProvider::create(jsCast(globalObject), source, JSC::SourceProviderSourceType::Program, isBuiltIn); + sourceOrigin = sourceProvider->sourceOrigin(); moduleObject = JSCommonJSModule::create( vm, globalObject->CommonJSModuleObjectStructure(), - requireMapKey, filename, dirname, JSC::JSSourceCode::create(vm, SourceCode(WTFMove(sourceProvider)))); + requireMapKey, filename, dirname, WTFMove(JSC::SourceCode(WTFMove(sourceProvider)))); moduleObject->putDirect(vm, WebCore::clientData(vm)->builtinNames().exportsPublicName(), JSC::constructEmptyObject(globalObject, globalObject->objectPrototype()), 0); globalObject->requireMap()->set(globalObject, requireMapKey, moduleObject); + } else { + sourceOrigin = Zig::toSourceOrigin(sourceURL, isBuiltIn); } moduleObject->ignoreESModuleAnnotation = ignoreESModuleAnnotation; @@ -1110,6 +1115,8 @@ std::optional createCommonJSModule( moduleObject->toSyntheticSource(globalObject, moduleKey, exportNames, exportValues); } + } else { + // require map was cleared of the entry } }, sourceOrigin, @@ -1134,7 +1141,7 @@ JSObject* JSCommonJSModule::createBoundRequireFunction(VM& vm, JSGlobalObject* l auto moduleObject = Bun::JSCommonJSModule::create( vm, globalObject->CommonJSModuleObjectStructure(), - filename, filename, dirname, nullptr); + filename, filename, dirname, SourceCode()); JSFunction* requireFunction = JSC::JSBoundFunction::create(vm, globalObject, diff --git a/src/bun.js/bindings/CommonJSModuleRecord.h b/src/bun.js/bindings/CommonJSModuleRecord.h index 530369bf67..ec95645231 100644 --- a/src/bun.js/bindings/CommonJSModuleRecord.h +++ b/src/bun.js/bindings/CommonJSModuleRecord.h @@ -1,6 +1,8 @@ #pragma once +#include "JavaScriptCore/JSGlobalObject.h" #include "root.h" #include "headers-handwritten.h" +#include "wtf/NakedPtr.h" namespace Zig { class GlobalObject; @@ -34,30 +36,34 @@ public: mutable JSC::WriteBarrier m_dirname; mutable JSC::WriteBarrier m_paths; mutable JSC::WriteBarrier m_parent; - mutable JSC::WriteBarrier sourceCode; bool ignoreESModuleAnnotation { false }; + JSC::SourceCode sourceCode = JSC::SourceCode(); + + void setSourceCode(JSC::SourceCode&& sourceCode); static void destroy(JSC::JSCell*); ~JSCommonJSModule(); + void clearSourceCode() { sourceCode = JSC::SourceCode(); } + void finishCreation(JSC::VM& vm, JSC::JSString* id, JSValue filename, - JSC::JSString* dirname, JSC::JSSourceCode* sourceCode); + JSC::JSString* dirname, const JSC::SourceCode& sourceCode); static JSC::Structure* createStructure(JSC::JSGlobalObject* globalObject); - bool evaluate(Zig::GlobalObject* globalObject, const WTF::String& sourceURL, ResolvedSource resolvedSource, bool isBuiltIn); - inline bool evaluate(Zig::GlobalObject* globalObject, const WTF::String& sourceURL, ResolvedSource resolvedSource) + bool evaluate(Zig::GlobalObject* globalObject, const WTF::String& sourceURL, ResolvedSource& resolvedSource, bool isBuiltIn); + inline bool evaluate(Zig::GlobalObject* globalObject, const WTF::String& sourceURL, ResolvedSource& resolvedSource) { return evaluate(globalObject, sourceURL, resolvedSource, false); } bool evaluate(Zig::GlobalObject* globalObject, const WTF::String& key, const SyntheticSourceProvider::SyntheticSourceGenerator& generator); - bool evaluate(Zig::GlobalObject* globalObject, const WTF::String& key, JSSourceCode* sourceCode); + bool evaluate(Zig::GlobalObject* globalObject, const WTF::String& key, const JSC::SourceCode& sourceCode); static JSCommonJSModule* create(JSC::VM& vm, JSC::Structure* structure, JSC::JSString* id, JSValue filename, - JSC::JSString* dirname, JSC::JSSourceCode* sourceCode); + JSC::JSString* dirname, const JSC::SourceCode& sourceCode); static JSCommonJSModule* create( Zig::GlobalObject* globalObject, @@ -79,10 +85,11 @@ public: JSValue exportsObject(); JSValue id(); + bool load(JSC::VM& vm, Zig::GlobalObject* globalObject, WTF::NakedPtr&); + DECLARE_INFO; DECLARE_VISIT_CHILDREN; - static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); template @@ -106,25 +113,21 @@ public: } }; -JSCommonJSModule* createCommonJSModuleWithoutRunning( - Zig::GlobalObject* globalObject, - Ref sourceProvider, - const WTF::String& sourceURL, - ResolvedSource source); - JSC::Structure* createCommonJSModuleStructure( Zig::GlobalObject* globalObject); std::optional createCommonJSModule( Zig::GlobalObject* globalObject, - ResolvedSource source, + JSC::JSValue specifierValue, + ResolvedSource& source, bool isBuiltIn); inline std::optional createCommonJSModule( Zig::GlobalObject* globalObject, - ResolvedSource source) + JSC::JSValue specifierValue, + ResolvedSource& source) { - return createCommonJSModule(globalObject, source, false); + return createCommonJSModule(globalObject, specifierValue, source, false); } class RequireResolveFunctionPrototype final : public JSC::JSNonFinalObject { diff --git a/src/bun.js/bindings/ModuleLoader.cpp b/src/bun.js/bindings/ModuleLoader.cpp index b1d4dfe6d0..338651f70d 100644 --- a/src/bun.js/bindings/ModuleLoader.cpp +++ b/src/bun.js/bindings/ModuleLoader.cpp @@ -1,8 +1,8 @@ #include "root.h" #include "headers-handwritten.h" - +#include "JavaScriptCore/JSGlobalObject.h" #include "ModuleLoader.h" - +#include "JavaScriptCore/Identifier.h" #include "ZigGlobalObject.h" #include #include @@ -290,8 +290,15 @@ static JSValue handleVirtualModuleResult( auto onLoadResult = handleOnLoadResult(globalObject, virtualModuleResult, specifier, wasModuleMock); JSC::VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); + WTF::String sourceCodeStringForDeref; + const auto getSourceCodeStringForDeref = [&]() { + if (res->success && res->result.value.needsDeref && res->result.value.source_code.tag == BunStringTag::WTFStringImpl) { + res->result.value.needsDeref = false; + sourceCodeStringForDeref = String(res->result.value.source_code.impl.wtf); + } + }; - auto reject = [&](JSC::JSValue exception) -> JSValue { + const auto reject = [&](JSC::JSValue exception) -> JSValue { if constexpr (allowPromise) { return rejectedInternalPromise(globalObject, exception); } else { @@ -300,7 +307,7 @@ static JSValue handleVirtualModuleResult( } }; - auto resolve = [&](JSValue code) -> JSValue { + const auto resolve = [&](JSValue code) -> JSValue { res->success = true; if constexpr (allowPromise) { scope.release(); @@ -310,7 +317,7 @@ static JSValue handleVirtualModuleResult( } }; - auto rejectOrResolve = [&](JSValue code) -> JSValue { + const auto rejectOrResolve = [&](JSValue code) -> JSValue { if (auto* exception = scope.exception()) { if constexpr (allowPromise) { scope.clearException(); @@ -336,6 +343,7 @@ static JSValue handleVirtualModuleResult( if (!res->success) { return reject(JSValue::decode(reinterpret_cast(res->result.err.ptr))); } + getSourceCodeStringForDeref(); auto provider = Zig::SourceProvider::create(globalObject, res->result.value); return resolve(JSC::JSSourceCode::create(vm, JSC::SourceCode(provider))); @@ -383,38 +391,62 @@ static JSValue handleVirtualModuleResult( } extern "C" void Bun__onFulfillAsyncModule( - JSC::EncodedJSValue promiseValue, + Zig::GlobalObject* globalObject, + JSC::EncodedJSValue encodedPromiseValue, ErrorableResolvedSource* res, BunString* specifier, BunString* referrer) { - JSC::JSValue value = JSValue::decode(promiseValue); - JSC::JSInternalPromise* promise = jsCast(value); - auto* globalObject = promise->globalObject(); + WTF::String sourceCodeStringForDeref; + const auto getSourceCodeStringForDeref = [&]() { + if (res->result.value.needsDeref && res->result.value.source_code.tag == BunStringTag::WTFStringImpl) { + res->result.value.needsDeref = false; + sourceCodeStringForDeref = String(res->result.value.source_code.impl.wtf); + } + }; auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); + JSC::JSInternalPromise* promise = jsCast(JSC::JSValue::decode(encodedPromiseValue)); if (!res->success) { throwException(scope, res->result.err, globalObject); auto* exception = scope.exception(); scope.clearException(); - return promise->reject(promise->globalObject(), exception); + return promise->reject(globalObject, exception); } - if (res->result.value.commonJSExportsLen) { - auto created = Bun::createCommonJSModule(jsCast(globalObject), res->result.value); + getSourceCodeStringForDeref(); + auto specifierValue = Bun::toJS(globalObject, *specifier); - if (created.has_value()) { - return promise->resolve(promise->globalObject(), JSSourceCode::create(vm, WTFMove(created.value()))); + if (auto entry = globalObject->esmRegistryMap()->get(globalObject, specifierValue)) { + if (res->result.value.commonJSExportsLen) { + if (entry.isObject()) { + if (auto isEvaluated = entry.getObject()->getIfPropertyExists(globalObject, Bun::builtinNames(vm).evaluatedPublicName())) { + if (isEvaluated.isTrue()) { + // it's a race! we lost. + // https://github.com/oven-sh/bun/issues/6946 + return; + } + } + } + + auto created = Bun::createCommonJSModule(jsCast(globalObject), specifierValue, res->result.value); + if (created.has_value()) { + JSSourceCode* code = JSSourceCode::create(vm, WTFMove(created.value())); + promise->resolve(globalObject, code); + } else { + auto* exception = scope.exception(); + scope.clearException(); + promise->reject(globalObject, exception); + } } else { - auto* exception = scope.exception(); - scope.clearException(); - return promise->reject(promise->globalObject(), exception); + auto&& provider = Zig::SourceProvider::create(jsDynamicCast(globalObject), res->result.value); + promise->resolve(globalObject, JSC::JSSourceCode::create(vm, JSC::SourceCode(provider))); } + } else { + // the module has since been deleted from the registry. + // let's not keep it forever for no reason. } - - auto provider = Zig::SourceProvider::create(jsDynamicCast(globalObject), res->result.value); - promise->resolve(promise->globalObject(), JSC::JSSourceCode::create(vm, JSC::SourceCode(provider))); } extern "C" bool isBunTest; @@ -432,7 +464,13 @@ JSValue fetchCommonJSModule( auto scope = DECLARE_THROW_SCOPE(vm); ErrorableResolvedSource resValue; ErrorableResolvedSource* res = &resValue; - + WTF::String sourceCodeStringForDeref; + const auto getSourceCodeStringForDeref = [&]() { + if (res->success && res->result.value.needsDeref && res->result.value.source_code.tag == BunStringTag::WTFStringImpl) { + res->result.value.needsDeref = false; + sourceCodeStringForDeref = String(res->result.value.source_code.impl.wtf); + } + }; auto& builtinNames = WebCore::clientData(vm)->builtinNames(); bool wasModuleMock = false; @@ -543,7 +581,7 @@ JSValue fetchCommonJSModule( JSMap* registry = globalObject->esmRegistryMap(); - auto hasAlreadyLoadedESMVersionSoWeShouldntTranspileItTwice = [&]() -> bool { + const auto hasAlreadyLoadedESMVersionSoWeShouldntTranspileItTwice = [&]() -> bool { JSValue entry = registry->get(globalObject, specifierValue); if (!entry || !entry.isObject()) { @@ -559,6 +597,7 @@ JSValue fetchCommonJSModule( } Bun__transpileFile(bunVM, globalObject, specifier, referrer, typeAttribute, res, false); + getSourceCodeStringForDeref(); if (res->success && res->result.value.commonJSExportsLen) { target->evaluate(globalObject, specifier->toWTFString(BunString::ZeroCopy), res->result.value); @@ -605,6 +644,7 @@ extern "C" bool isBunTest; template static JSValue fetchESMSourceCode( Zig::GlobalObject* globalObject, + JSC::JSValue specifierJS, ErrorableResolvedSource* res, BunString* specifier, BunString* referrer, @@ -614,16 +654,16 @@ static JSValue fetchESMSourceCode( auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - auto reject = [&](JSC::JSValue exception) -> JSValue { + const auto reject = [&](JSC::JSValue exception) -> JSValue { if constexpr (allowPromise) { return rejectedInternalPromise(globalObject, exception); } else { throwException(globalObject, scope, exception); - return JSC::jsUndefined(); + return {}; } }; - auto resolve = [&](JSValue code) -> JSValue { + const auto resolve = [&](JSValue code) -> JSValue { if constexpr (allowPromise) { auto* ret = resolvedInternalPromise(globalObject, code); scope.release(); @@ -633,7 +673,7 @@ static JSValue fetchESMSourceCode( } }; - auto rejectOrResolve = [&](JSValue code) -> JSValue { + const auto rejectOrResolve = [&](JSValue code) -> JSValue { if (auto* exception = scope.exception()) { if constexpr (!allowPromise) { scope.release(); @@ -709,17 +749,27 @@ static JSValue fetchESMSourceCode( } } + WTF::String sourceCodeStringForDeref; + const auto getSourceCodeStringForDeref = [&]() { + if (res->success && res->result.value.needsDeref && res->result.value.source_code.tag == BunStringTag::WTFStringImpl) { + res->result.value.needsDeref = false; + sourceCodeStringForDeref = String(res->result.value.source_code.impl.wtf); + } + }; + if constexpr (allowPromise) { - void* pendingCtx = Bun__transpileFile(bunVM, globalObject, specifier, referrer, typeAttribute, res, true); + auto* pendingCtx = Bun__transpileFile(bunVM, globalObject, specifier, referrer, typeAttribute, res, true); + getSourceCodeStringForDeref(); if (pendingCtx) { - return reinterpret_cast(pendingCtx); + return pendingCtx; } } else { Bun__transpileFile(bunVM, globalObject, specifier, referrer, typeAttribute, res, false); + getSourceCodeStringForDeref(); } if (res->success && res->result.value.commonJSExportsLen) { - auto created = Bun::createCommonJSModule(globalObject, res->result.value); + auto created = Bun::createCommonJSModule(globalObject, specifierJS, res->result.value); if (created.has_value()) { return rejectOrResolve(JSSourceCode::create(vm, WTFMove(created.value()))); @@ -730,7 +780,7 @@ static JSValue fetchESMSourceCode( scope.clearException(); return rejectedInternalPromise(globalObject, exception); } else { - return JSC::jsUndefined(); + return {}; } } @@ -769,8 +819,8 @@ static JSValue fetchESMSourceCode( return rejectOrResolve(JSSourceCode::create(globalObject->vm(), WTFMove(source))); } - auto&& provider = Zig::SourceProvider::create(globalObject, res->result.value); - return rejectOrResolve(JSC::JSSourceCode::create(vm, JSC::SourceCode(provider))); + return rejectOrResolve(JSC::JSSourceCode::create(vm, + JSC::SourceCode(Zig::SourceProvider::create(globalObject, res->result.value)))); } extern "C" JSC::EncodedJSValue jsFunctionOnLoadObjectResultResolve(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame) @@ -828,21 +878,23 @@ extern "C" JSC::EncodedJSValue jsFunctionOnLoadObjectResultReject(JSC::JSGlobalO JSValue fetchESMSourceCodeSync( Zig::GlobalObject* globalObject, + JSC::JSValue specifierJS, ErrorableResolvedSource* res, BunString* specifier, BunString* referrer, BunString* typeAttribute) { - return fetchESMSourceCode(globalObject, res, specifier, referrer, typeAttribute); + return fetchESMSourceCode(globalObject, specifierJS, res, specifier, referrer, typeAttribute); } JSValue fetchESMSourceCodeAsync( Zig::GlobalObject* globalObject, + JSC::JSValue specifierJS, ErrorableResolvedSource* res, BunString* specifier, BunString* referrer, BunString* typeAttribute) { - return fetchESMSourceCode(globalObject, res, specifier, referrer, typeAttribute); + return fetchESMSourceCode(globalObject, specifierJS, res, specifier, referrer, typeAttribute); } } diff --git a/src/bun.js/bindings/ModuleLoader.h b/src/bun.js/bindings/ModuleLoader.h index 8a04a2bb75..2212bab10c 100644 --- a/src/bun.js/bindings/ModuleLoader.h +++ b/src/bun.js/bindings/ModuleLoader.h @@ -88,6 +88,7 @@ public: JSValue fetchESMSourceCodeSync( Zig::GlobalObject* globalObject, + JSValue spceifierJS, ErrorableResolvedSource* res, BunString* specifier, BunString* referrer, @@ -95,6 +96,7 @@ JSValue fetchESMSourceCodeSync( JSValue fetchESMSourceCodeAsync( Zig::GlobalObject* globalObject, + JSValue spceifierJS, ErrorableResolvedSource* res, BunString* specifier, BunString* referrer, diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 42bdf1fc33..f1fa0a0269 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -814,8 +814,9 @@ extern "C" JSC__JSGlobalObject* Zig__GlobalObject__create(void* console_client, } JSC_DEFINE_HOST_FUNCTION(functionFulfillModuleSync, - (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) + (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) { + Zig::GlobalObject* globalObject = jsCast(lexicalGlobalObject); auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); @@ -836,7 +837,8 @@ JSC_DEFINE_HOST_FUNCTION(functionFulfillModuleSync, res.result.err.ptr = nullptr; JSValue result = Bun::fetchESMSourceCodeSync( - reinterpret_cast(globalObject), + globalObject, + key, &res, &specifier, &specifier, @@ -4480,6 +4482,7 @@ JSC::JSInternalPromise* GlobalObject::moduleLoaderFetch(JSGlobalObject* globalOb JSValue result = Bun::fetchESMSourceCodeAsync( reinterpret_cast(globalObject), + key, &res, &moduleKeyBun, &source, diff --git a/src/bun.js/bindings/ZigSourceProvider.cpp b/src/bun.js/bindings/ZigSourceProvider.cpp index d3afd80811..a501a1f28b 100644 --- a/src/bun.js/bindings/ZigSourceProvider.cpp +++ b/src/bun.js/bindings/ZigSourceProvider.cpp @@ -6,6 +6,7 @@ #include #include "ZigGlobalObject.h" +#include "wtf/Assertions.h" #include #include @@ -43,7 +44,7 @@ static uintptr_t getSourceProviderMapKey(ResolvedSource& resolvedSource) } } -static SourceOrigin toSourceOrigin(const String& sourceURL, bool isBuiltin) +SourceOrigin toSourceOrigin(const String& sourceURL, bool isBuiltin) { if (isBuiltin) { if (sourceURL.startsWith("node:"_s)) { @@ -55,6 +56,7 @@ static SourceOrigin toSourceOrigin(const String& sourceURL, bool isBuiltin) } } + ASSERT_WITH_MESSAGE(!sourceURL.startsWith("file://"_s), "sourceURL should not already be a file URL"); return SourceOrigin(WTF::URL::fileURLWithFileSystemPath(sourceURL)); } @@ -92,7 +94,7 @@ JSC::SourceID sourceIDForSourceURL(const WTF::String& sourceURL) extern "C" bool BunTest__shouldGenerateCodeCoverage(BunString sourceURL); -Ref SourceProvider::create(Zig::GlobalObject* globalObject, ResolvedSource resolvedSource, JSC::SourceProviderSourceType sourceType, bool isBuiltin) +Ref SourceProvider::create(Zig::GlobalObject* globalObject, ResolvedSource& resolvedSource, JSC::SourceProviderSourceType sourceType, bool isBuiltin) { auto string = resolvedSource.source_code.toWTFString(BunString::ZeroCopy); @@ -103,8 +105,8 @@ Ref SourceProvider::create(Zig::GlobalObject* globalObject, Reso bool shouldGenerateCodeCoverage = isCodeCoverageEnabled && !isBuiltin && BunTest__shouldGenerateCodeCoverage(resolvedSource.source_url); if (resolvedSource.needsDeref && !isBuiltin) { + resolvedSource.needsDeref = false; resolvedSource.source_code.deref(); - // Do not deref either source_url or specifier // Specifier's lifetime is the JSValue, mostly // source_url is owned by the string above diff --git a/src/bun.js/bindings/ZigSourceProvider.h b/src/bun.js/bindings/ZigSourceProvider.h index c4ca3748b0..4469486bf5 100644 --- a/src/bun.js/bindings/ZigSourceProvider.h +++ b/src/bun.js/bindings/ZigSourceProvider.h @@ -23,7 +23,7 @@ class GlobalObject; void forEachSourceProvider(WTF::Function); JSC::SourceID sourceIDForSourceURL(const WTF::String& sourceURL); void* sourceMappingForSourceURL(const WTF::String& sourceURL); - +JSC::SourceOrigin toSourceOrigin(const String& sourceURL, bool isBuiltin); class SourceProvider final : public JSC::SourceProvider { WTF_MAKE_FAST_ALLOCATED; using Base = JSC::SourceProvider; @@ -36,7 +36,7 @@ class SourceProvider final : public JSC::SourceProvider { using SourceOrigin = JSC::SourceOrigin; public: - static Ref create(Zig::GlobalObject*, ResolvedSource resolvedSource, JSC::SourceProviderSourceType sourceType = JSC::SourceProviderSourceType::Module, bool isBuiltIn = false); + static Ref create(Zig::GlobalObject*, ResolvedSource& resolvedSource, JSC::SourceProviderSourceType sourceType = JSC::SourceProviderSourceType::Module, bool isBuiltIn = false); ~SourceProvider(); unsigned hash() const override; StringView source() const override { return StringView(m_source.get()); } diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 0d9be4c9f4..d24d06b2e0 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -1,5 +1,7 @@ + #include "root.h" +#include "JavaScriptCore/DeleteAllCodeEffort.h" #include "headers.h" #include "BunClientData.h" @@ -3060,7 +3062,7 @@ void JSC__JSInternalPromise__rejectAsHandledException(JSC__JSInternalPromise* ar JSC__JSInternalPromise* JSC__JSInternalPromise__rejectedPromise(JSC__JSGlobalObject* arg0, JSC__JSValue JSValue1) { - return reinterpret_cast( + return jsCast( JSC::JSInternalPromise::rejectedPromise(arg0, JSC::JSValue::decode(JSValue1))); } @@ -4587,8 +4589,11 @@ JSC__JSValue JSC__VM__runGC(JSC__VM* vm, bool sync) WTF::releaseFastMallocFreeMemory(); if (sync) { + vm->clearSourceProviderCaches(); + vm->heap.deleteAllUnlinkedCodeBlocks(JSC::PreventCollectionAndDeleteAllCode); vm->heap.collectNow(JSC::Sync, JSC::CollectionScope::Full); } else { + vm->heap.deleteAllUnlinkedCodeBlocks(JSC::DeleteAllCodeIfNotCollecting); vm->heap.collectSync(JSC::CollectionScope::Full); } diff --git a/src/bun.js/bindings/headers-handwritten.h b/src/bun.js/bindings/headers-handwritten.h index aef02528ea..e752349035 100644 --- a/src/bun.js/bindings/headers-handwritten.h +++ b/src/bun.js/bindings/headers-handwritten.h @@ -311,7 +311,7 @@ extern "C" JSC::EncodedJSValue Bun__runVirtualModule( JSC::JSGlobalObject* global, const BunString* specifier); -extern "C" void* Bun__transpileFile( +extern "C" JSC::JSInternalPromise* Bun__transpileFile( void* bunVM, JSC::JSGlobalObject* global, const BunString* specifier, diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index d8bdfb1c93..b6a164e13b 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -363,6 +363,7 @@ const ShellIOWriterAsyncDeinit = bun.shell.Interpreter.AsyncDeinitWriter; const TimerReference = JSC.BunTimer.Timeout.TimerReference; const ProcessWaiterThreadTask = if (Environment.isPosix) bun.spawn.WaiterThread.ProcessQueue.ResultTask else opaque {}; const ProcessMiniEventLoopWaiterThreadTask = if (Environment.isPosix) bun.spawn.WaiterThread.ProcessMiniEventLoopQueue.ResultTask else opaque {}; +const RuntimeTranspilerStore = JSC.RuntimeTranspilerStore; // Task.get(ReadFileTask) -> ?ReadFileTask pub const Task = TaggedPointerUnion(.{ FetchTasklet, @@ -434,6 +435,7 @@ pub const Task = TaggedPointerUnion(.{ TimerReference, ProcessWaiterThreadTask, + RuntimeTranspilerStore, }); const UnboundedQueue = @import("./unbounded_queue.zig").UnboundedQueue; pub const ConcurrentTask = struct { @@ -773,8 +775,7 @@ pub const EventLoop = struct { defer this.debug.exit(); if (count == 1) { - this.global.vm().releaseWeakRefs(); - this.drainMicrotasksWithGlobal(this.global); + this.drainMicrotasksWithGlobal(this.global, this.virtual_machine.jsc); } this.entered_event_loop_count -= 1; @@ -807,9 +808,10 @@ pub const EventLoop = struct { } extern fn JSC__JSGlobalObject__drainMicrotasks(*JSC.JSGlobalObject) void; - fn drainMicrotasksWithGlobal(this: *EventLoop, globalObject: *JSC.JSGlobalObject) void { + pub fn drainMicrotasksWithGlobal(this: *EventLoop, globalObject: *JSC.JSGlobalObject, jsc_vm: *JSC.VM) void { JSC.markBinding(@src()); + jsc_vm.releaseWeakRefs(); JSC__JSGlobalObject__drainMicrotasks(globalObject); this.deferred_tasks.run(); @@ -819,8 +821,7 @@ pub const EventLoop = struct { } pub fn drainMicrotasks(this: *EventLoop) void { - this.virtual_machine.jsc.releaseWeakRefs(); - this.drainMicrotasksWithGlobal(this.global); + this.drainMicrotasksWithGlobal(this.global, this.virtual_machine.jsc); } /// When you call a JavaScript function from outside the event loop task @@ -845,7 +846,7 @@ pub const EventLoop = struct { pub fn tickQueueWithCount(this: *EventLoop, comptime queue_name: []const u8) u32 { var global = this.global; - var global_vm = global.vm(); + const global_vm = global.vm(); var counter: usize = 0; if (comptime Environment.isDebug) { @@ -1171,6 +1172,10 @@ pub const EventLoop = struct { var any: *ProcessWaiterThreadTask = task.get(ProcessWaiterThreadTask).?; any.runFromJSThread(); }, + @field(Task.Tag, typeBaseName(@typeName(RuntimeTranspilerStore))) => { + var any: *RuntimeTranspilerStore = task.get(RuntimeTranspilerStore).?; + any.drain(); + }, @field(Task.Tag, typeBaseName(@typeName(TimerReference))) => { bun.markPosixOnly(); var any: *TimerReference = task.get(TimerReference).?; @@ -1185,8 +1190,7 @@ pub const EventLoop = struct { }, } - global_vm.releaseWeakRefs(); - this.drainMicrotasksWithGlobal(global); + this.drainMicrotasksWithGlobal(global, global_vm); } @field(this, queue_name).head = if (@field(this, queue_name).count == 0) 0 else @field(this, queue_name).head; @@ -1424,8 +1428,7 @@ pub const EventLoop = struct { while (this.tickWithCount() > 0) : (this.global.handleRejectedPromises()) { this.tickConcurrent(); } else { - global_vm.releaseWeakRefs(); - this.drainMicrotasksWithGlobal(global); + this.drainMicrotasksWithGlobal(global, global_vm); this.tickConcurrent(); if (this.tasks.count > 0) continue; } diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index 72dbddc32d..513f9bd794 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -86,6 +86,8 @@ const Dependency = @import("../install/dependency.zig"); const Async = bun.Async; const String = bun.String; +const debug = Output.scoped(.ModuleLoader, true); + // Setting BUN_OVERRIDE_MODULE_PATH to the path to the bun repo will make it so modules are loaded // from there instead of the ones embedded into the binary. // In debug mode, this is set automatically for you, using the path relative to this file. @@ -217,11 +219,12 @@ fn setBreakPointOnFirstLine() bool { } pub const RuntimeTranspilerStore = struct { - const debug = Output.scoped(.compile, false); - generation_number: std.atomic.Value(u32) = std.atomic.Value(u32).init(0), store: TranspilerJob.Store, enabled: bool = true, + queue: Queue = Queue{}, + + pub const Queue = bun.UnboundedQueue(TranspilerJob, .next); pub fn init(allocator: std.mem.Allocator) RuntimeTranspilerStore { return RuntimeTranspilerStore{ @@ -229,6 +232,29 @@ pub const RuntimeTranspilerStore = struct { }; } + // Thsi is run at the top of the event loop on the JS thread. + pub fn drain(this: *RuntimeTranspilerStore) void { + var batch = this.queue.popBatch(); + var iter = batch.iterator(); + if (iter.next()) |job| { + // we run just one job first to see if there are more + job.runFromJSThread(); + } else { + return; + } + var vm = @fieldParentPtr(JSC.VirtualMachine, "transpiler_store", this); + const event_loop = vm.eventLoop(); + const global = vm.global; + const jsc_vm = vm.jsc; + while (iter.next()) |job| { + // if there are more, we need to drain the microtasks from the previous run + event_loop.drainMicrotasksWithGlobal(global, jsc_vm); + job.runFromJSThread(); + } + + // immediately after this is called, the microtasks will be drained again. + } + pub fn transpile( this: *RuntimeTranspilerStore, vm: *JSC.VirtualMachine, @@ -236,7 +262,6 @@ pub const RuntimeTranspilerStore = struct { path: Fs.Path, referrer: []const u8, ) *anyopaque { - debug("transpile({s})", .{path.text}); var job: *TranspilerJob = this.store.get(); const owned_path = Fs.Path.init(bun.default_allocator.dupe(u8, path.text) catch unreachable); const promise = JSC.JSInternalPromise.create(globalObject); @@ -253,6 +278,8 @@ pub const RuntimeTranspilerStore = struct { .file = {}, }, }; + if (comptime Environment.allow_assert) + debug("transpile({s}, {s}, async)", .{ path.text, @tagName(job.loader) }); job.schedule(); return promise; } @@ -271,6 +298,7 @@ pub const RuntimeTranspilerStore = struct { parse_error: ?anyerror = null, resolved_source: ResolvedSource = ResolvedSource{}, work_task: JSC.WorkPoolTask = .{ .callback = runFromWorkerThread }, + next: ?*TranspilerJob = null, pub const Store = bun.HiveArray(TranspilerJob, 64).Fallback; @@ -302,9 +330,8 @@ pub const RuntimeTranspilerStore = struct { threadlocal var source_code_printer: ?*js_printer.BufferPrinter = null; pub fn dispatchToMainThread(this: *TranspilerJob) void { - this.vm.eventLoop().enqueueTaskConcurrent( - JSC.ConcurrentTask.fromCallback(this, runFromJSThread), - ); + this.vm.transpiler_store.queue.push(this); + this.vm.eventLoop().enqueueTaskConcurrent(JSC.ConcurrentTask.createFrom(&this.vm.transpiler_store)); } pub fn runFromJSThread(this: *TranspilerJob) void { @@ -413,7 +440,7 @@ pub const RuntimeTranspilerStore = struct { .hot, .watch => { if (vm.bun_watcher.indexOf(hash)) |index| { const _fd = vm.bun_watcher.watchlist().items(.fd)[index]; - fd = if (_fd.int() > 0) _fd else null; + fd = if (!_fd.isStdio()) _fd else null; package_json = vm.bun_watcher.watchlist().items(.package_json)[index]; } }, @@ -567,6 +594,7 @@ pub const RuntimeTranspilerStore = struct { .source_url = duped.createIfDifferent(path.text), .hash = 0, }; + this.resolved_source.source_code.value.WTFStringImpl.ensureHash(); return; } @@ -637,20 +665,31 @@ pub const RuntimeTranspilerStore = struct { } const duped = String.createUTF8(specifier); + const source_code = brk: { + const written = printer.ctx.getWritten(); + + const result = cache.output_code orelse bun.String.createLatin1(written); + + if (written.len > 1024 * 1024 * 2 or vm.smol) { + printer.ctx.buffer.deinit(); + source_code_printer.?.* = printer; + } + + // In a benchmarking loading @babel/standalone 100 times: + // + // After ensureHash: + // 354.00 ms 4.2% 354.00 ms WTF::StringImpl::hashSlowCase() const + // + // Before ensureHash: + // 506.00 ms 6.1% 506.00 ms WTF::StringImpl::hashSlowCase() const + // + result.value.WTFStringImpl.ensureHash(); + + break :brk result; + }; this.resolved_source = ResolvedSource{ .allocator = null, - .source_code = brk: { - const written = printer.ctx.getWritten(); - - const result = cache.output_code orelse bun.String.createLatin1(written); - - if (written.len > 1024 * 1024 * 2 or vm.smol) { - printer.ctx.buffer.deinit(); - source_code_printer.?.* = printer; - } - - break :brk result; - }, + .source_code = source_code, .specifier = duped, .source_url = duped.createIfDifferent(path.text), .commonjs_exports = null, @@ -668,8 +707,6 @@ pub const ModuleLoader = struct { transpile_source_code_arena: ?*bun.ArenaAllocator = null, eval_source: ?*logger.Source = null, - const debug = Output.scoped(.ModuleLoader, true); - /// This must be called after calling transpileSourceCode pub fn resetArena(this: *ModuleLoader, jsc_vm: *VirtualMachine) void { std.debug.assert(&jsc_vm.module_loader == this); @@ -1065,6 +1102,7 @@ pub const ModuleLoader = struct { var spec = bun.String.init(ZigString.init(this.specifier).withEncoding()); var ref = bun.String.init(ZigString.init(this.referrer).withEncoding()); Bun__onFulfillAsyncModule( + this.globalThis, this.promise.get().?, &errorable, &spec, @@ -1107,6 +1145,7 @@ pub const ModuleLoader = struct { log.deinit(); Bun__onFulfillAsyncModule( + globalThis, promise, &errorable, &specifier, @@ -1419,6 +1458,7 @@ pub const ModuleLoader = struct { } extern "C" fn Bun__onFulfillAsyncModule( + globalObject: *JSC.JSGlobalObject, promiseValue: JSC.JSValue, res: *JSC.ErrorableResolvedSource, specifier: *bun.String, @@ -2135,7 +2175,6 @@ pub const ModuleLoader = struct { JSC.markBinding(@src()); var log = logger.Log.init(jsc_vm.bundler.allocator); defer log.deinit(); - debug("transpileFile: {any}", .{specifier_ptr.*}); var _specifier = specifier_ptr.toUTF8(jsc_vm.allocator); var referrer_slice = referrer.toUTF8(jsc_vm.allocator); @@ -2212,6 +2251,9 @@ pub const ModuleLoader = struct { } }; + if (comptime Environment.allow_assert) + debug("transpile({s}, {s}, sync)", .{ specifier, @tagName(synchronous_loader) }); + defer jsc_vm.module_loader.resetArena(jsc_vm); var promise: ?*JSC.JSInternalPromise = null; diff --git a/src/bun.js/modules/NodeModuleModule.h b/src/bun.js/modules/NodeModuleModule.h index 3fb5e72f94..1891723ec6 100644 --- a/src/bun.js/modules/NodeModuleModule.h +++ b/src/bun.js/modules/NodeModuleModule.h @@ -124,7 +124,7 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeModuleModuleConstructor, } auto *out = Bun::JSCommonJSModule::create(vm, structure, idString, jsNull(), - dirname, nullptr); + dirname, SourceCode()); if (!parentValue.isUndefined()) out->putDirect(vm, JSC::Identifier::fromString(vm, "parent"_s), parentValue, diff --git a/src/js/builtins/BunBuiltinNames.h b/src/js/builtins/BunBuiltinNames.h index 41dd00d54c..127524b40c 100644 --- a/src/js/builtins/BunBuiltinNames.h +++ b/src/js/builtins/BunBuiltinNames.h @@ -20,24 +20,10 @@ namespace WebCore { using namespace JSC; #define BUN_COMMON_PRIVATE_IDENTIFIERS_EACH_PROPERTY_NAME(macro) \ - macro(AbortSignal) \ - macro(Buffer) \ - macro(Bun) \ - macro(Loader) \ - macro(ReadableByteStreamController) \ - macro(ReadableStream) \ - macro(ReadableStreamBYOBReader) \ - macro(ReadableStreamBYOBRequest) \ - macro(ReadableStreamDefaultController) \ - macro(ReadableStreamDefaultReader) \ - macro(TransformStream) \ - macro(TransformStreamDefaultController) \ - macro(WritableStream) \ - macro(WritableStreamDefaultController) \ - macro(WritableStreamDefaultWriter) \ macro(__esModule) \ macro(_events) \ macro(abortAlgorithm) \ + macro(AbortSignal) \ macro(abortSteps) \ macro(addEventListener) \ macro(appendFromJS) \ @@ -49,6 +35,8 @@ using namespace JSC; macro(backpressureChangePromise) \ macro(basename) \ macro(body) \ + macro(Buffer) \ + macro(Bun) \ macro(bunNativePtr) \ macro(bunNativeType) \ macro(byobRequest) \ @@ -58,11 +46,11 @@ using namespace JSC; macro(cloneArrayBuffer) \ macro(close) \ macro(closeAlgorithm) \ - macro(closeRequest) \ - macro(closeRequested) \ macro(closed) \ macro(closedPromise) \ macro(closedPromiseCapability) \ + macro(closeRequest) \ + macro(closeRequested) \ macro(code) \ macro(connect) \ macro(consumeReadableStream) \ @@ -75,8 +63,8 @@ using namespace JSC; macro(createInternalModuleById) \ macro(createNativeReadableStream) \ macro(createReadableStream) \ - macro(createUsedReadableStream) \ macro(createUninitializedArrayBuffer) \ + macro(createUsedReadableStream) \ macro(createWritableStreamFromInternal) \ macro(cwd) \ macro(data) \ @@ -95,6 +83,7 @@ using namespace JSC; macro(errno) \ macro(errorSteps) \ macro(evaluateCommonJSModule) \ + macro(evaluated) \ macro(execArgv) \ macro(exports) \ macro(extname) \ @@ -138,13 +127,14 @@ using namespace JSC; macro(lazy) \ macro(lazyStreamPrototypeMap) \ macro(loadCJS2ESM) \ + macro(Loader) \ macro(localStreams) \ macro(main) \ macro(makeDOMException) \ macro(makeGetterTypeError) \ macro(makeThisTypeError) \ - macro(mockedFunction) \ macro(method) \ + macro(mockedFunction) \ macro(nextTick) \ macro(normalize) \ macro(on) \ @@ -177,12 +167,18 @@ using namespace JSC; macro(put) \ macro(queue) \ macro(read) \ - macro(readIntoRequests) \ - macro(readRequests) \ macro(readable) \ + macro(ReadableByteStreamController) \ + macro(ReadableStream) \ + macro(ReadableStreamBYOBReader) \ + macro(ReadableStreamBYOBRequest) \ macro(readableStreamController) \ + macro(ReadableStreamDefaultController) \ + macro(ReadableStreamDefaultReader) \ macro(readableStreamToArray) \ macro(reader) \ + macro(readIntoRequests) \ + macro(readRequests) \ macro(readyPromise) \ macro(readyPromiseCapability) \ macro(redirect) \ @@ -226,6 +222,8 @@ using namespace JSC; macro(toNamespacedPath) \ macro(trace) \ macro(transformAlgorithm) \ + macro(TransformStream) \ + macro(TransformStreamDefaultController) \ macro(uncork) \ macro(underlyingByteSource) \ macro(underlyingSink) \ @@ -239,10 +237,13 @@ using namespace JSC; macro(view) \ macro(whenSignalAborted) \ macro(writable) \ + macro(WritableStream) \ + macro(WritableStreamDefaultController) \ + macro(WritableStreamDefaultWriter) \ macro(write) \ macro(writeAlgorithm) \ - macro(writeRequests) \ macro(writer) \ + macro(writeRequests) \ macro(writing) \ macro(written) \ diff --git a/src/js/builtins/ImportMetaObject.ts b/src/js/builtins/ImportMetaObject.ts index aefd41fb09..0b502c2000 100644 --- a/src/js/builtins/ImportMetaObject.ts +++ b/src/js/builtins/ImportMetaObject.ts @@ -4,32 +4,63 @@ $visibility = "Private"; export function loadCJS2ESM(this: ImportMetaObject, resolvedSpecifier: string) { var loader = Loader; var queue = $createFIFO(); - var key = resolvedSpecifier; + let key = resolvedSpecifier; + const registry = loader.registry; + while (key) { // we need to explicitly check because state could be $ModuleFetch // it will throw this error if we do not: // $throwTypeError("Requested module is already fetched."); - var entry = loader.registry.$get(key)!; + let entry = registry.$get(key)!, + moduleRecordPromise, + state = 0, + // entry.fetch is a Promise + // SourceCode is not a string, it's a JSC::SourceCode object + fetch: Promise | undefined; - if ((entry?.state ?? 0) <= $ModuleFetch) { - $fulfillModuleSync(key); - entry = loader.registry.$get(key)!; + if (entry) { + ({ state, fetch } = entry); } - // entry.fetch is a Promise - // SourceCode is not a string, it's a JSC::SourceCode object - // this pulls it out of the promise without delaying by a tick - // the promise is already fullfilled by $fullfillModuleSync - var sourceCodeObject = $getPromiseInternalField(entry.fetch, $promiseFieldReactionsOrResult); - // parseModule() returns a Promise, but the value is already fulfilled - // so we just pull it out of the promise here once again - // But, this time we do it a little more carefully because this is a JSC function call and not bun source code - var moduleRecordPromise = loader.parseModule(key, sourceCodeObject); - var mod = entry.module; + if ( + !entry || + // if we need to fetch it + (state <= $ModuleFetch && + // either: + // - we've never fetched it + // - a fetch is in progress + (!$isPromise(fetch) || + ($getPromiseInternalField(fetch, $promiseFieldFlags) & $promiseStateMask) === $promiseStatePending)) + ) { + // force it to be no longer pending + $fulfillModuleSync(key); + + entry = registry.$get(key)!; + + // the state can transition here + // https://github.com/oven-sh/bun/issues/8965 + if (entry) { + ({ state = 0, fetch } = entry); + } + } + + if (state < $ModuleLink && $isPromise(fetch)) { + // This will probably never happen, but just in case + if (($getPromiseInternalField(fetch, $promiseFieldFlags) & $promiseStateMask) === $promiseStatePending) { + throw new TypeError(`require() async module "${key}" is unsupported. use "await import()" instead.`); + } + + // this pulls it out of the promise without delaying by a tick + // the promise is already fullfilled by $fullfillModuleSync + const sourceCodeObject = $getPromiseInternalField(fetch, $promiseFieldReactionsOrResult); + moduleRecordPromise = loader.parseModule(key, sourceCodeObject); + } + let mod = entry?.module; + if (moduleRecordPromise && $isPromise(moduleRecordPromise)) { - var reactionsOrResult = $getPromiseInternalField(moduleRecordPromise, $promiseFieldReactionsOrResult); - var flags = $getPromiseInternalField(moduleRecordPromise, $promiseFieldFlags); - var state = flags & $promiseStateMask; + let reactionsOrResult = $getPromiseInternalField(moduleRecordPromise, $promiseFieldReactionsOrResult); + let flags = $getPromiseInternalField(moduleRecordPromise, $promiseFieldFlags); + let state = flags & $promiseStateMask; // this branch should never happen, but just to be safe if (state === $promiseStatePending || (reactionsOrResult && $isPromise(reactionsOrResult))) { throw new TypeError(`require() async module "${key}" is unsupported. use "await import()" instead.`); @@ -51,15 +82,15 @@ export function loadCJS2ESM(this: ImportMetaObject, resolvedSpecifier: string) { // This is very similar to "requestInstantiate" in ModuleLoader.js in JavaScriptCore. $setStateToMax(entry, $ModuleLink); - var dependenciesMap = mod.dependenciesMap; - var requestedModules = loader.requestedModules(mod); - var dependencies = $newArrayWithSize(requestedModules.length); + const dependenciesMap = mod.dependenciesMap; + const requestedModules = loader.requestedModules(mod); + const dependencies = $newArrayWithSize(requestedModules.length); for (var i = 0, length = requestedModules.length; i < length; ++i) { - var depName = requestedModules[i]; + const depName = requestedModules[i]; // optimization: if it starts with a slash then it's an absolute path // we don't need to run the resolver a 2nd time - var depKey = depName[0] === "/" ? depName : loader.resolve(depName, key); - var depEntry = loader.ensureRegistered(depKey); + const depKey = depName[0] === "/" ? depName : loader.resolve(depName, key); + const depEntry = loader.ensureRegistered(depKey); if (depEntry.state < $ModuleLink) { queue.push(depKey); @@ -76,7 +107,7 @@ export function loadCJS2ESM(this: ImportMetaObject, resolvedSpecifier: string) { entry.isSatisfied = true; key = queue.shift(); - while (key && (loader.registry.$get(key)?.state ?? $ModuleFetch) >= $ModuleLink) { + while (key && (registry.$get(key)?.state ?? $ModuleFetch) >= $ModuleLink) { key = queue.shift(); } } @@ -90,7 +121,7 @@ export function loadCJS2ESM(this: ImportMetaObject, resolvedSpecifier: string) { ); } - return loader.registry.$get(resolvedSpecifier); + return registry.$get(resolvedSpecifier); } $visibility = "Private"; diff --git a/src/string.zig b/src/string.zig index a0fb5660fe..662e6b302f 100644 --- a/src/string.zig +++ b/src/string.zig @@ -121,6 +121,13 @@ pub const WTFStringImplStruct = extern struct { return ZigString.Slice.init(this.refCountAllocator(), this.latin1Slice()); } + extern fn Bun__WTFStringImpl__ensureHash(this: WTFStringImpl) void; + /// Compute the hash() if necessary + pub fn ensureHash(this: WTFStringImpl) void { + JSC.markBinding(@src()); + Bun__WTFStringImpl__ensureHash(this); + } + pub fn toUTF8(this: WTFStringImpl, allocator: std.mem.Allocator) ZigString.Slice { if (this.is8Bit()) { if (bun.strings.toUTF8FromLatin1(allocator, this.latin1Slice()) catch bun.outOfMemory()) |utf8| { diff --git a/test/js/bun/util/text-loader-fixture-dynamic-import-stress.ts b/test/js/bun/util/text-loader-fixture-dynamic-import-stress.ts index 0eb376132d..f81c02a7e8 100644 --- a/test/js/bun/util/text-loader-fixture-dynamic-import-stress.ts +++ b/test/js/bun/util/text-loader-fixture-dynamic-import-stress.ts @@ -1,7 +1,8 @@ -const count = process.platform === "win32" ? 10_000 : 100_000; +const count = process.platform === "win32" ? 1000 : 10_000; for (let i = 0; i < count; i++) { await import("./text-loader-fixture-text-file.txt?" + i++); } +Bun.gc(true); const { default: text } = await import("./text-loader-fixture-text-file.txt"); diff --git a/test/regression/issue/06946/06946.test.ts b/test/regression/issue/06946/06946.test.ts new file mode 100644 index 0000000000..0b1ab615c7 --- /dev/null +++ b/test/regression/issue/06946/06946.test.ts @@ -0,0 +1,19 @@ +import { test, expect } from "bun:test"; +import { bunEnv, bunExe } from "harness"; +import { join } from "path"; + +test("06946", async () => { + const buns = Array.from( + { length: 25 }, + () => + Bun.spawn({ + cmd: [bunExe(), join(import.meta.dir, "t.mjs")], + cwd: import.meta.dir, + stdio: ["inherit", "inherit", "inherit"], + env: bunEnv, + }).exited, + ); + + const exited = await Promise.all(buns); + expect(exited).toEqual(Array.from({ length: 25 }, () => 0)); +}); diff --git a/test/regression/issue/06946/l.js b/test/regression/issue/06946/l.js new file mode 100644 index 0000000000..4e744e91ad --- /dev/null +++ b/test/regression/issue/06946/l.js @@ -0,0 +1,3 @@ +class Logger {} +exports.Logger = Logger; +console.log("l.js has loaded"); diff --git a/test/regression/issue/06946/t.mjs b/test/regression/issue/06946/t.mjs new file mode 100644 index 0000000000..3a145ad48d --- /dev/null +++ b/test/regression/issue/06946/t.mjs @@ -0,0 +1,4 @@ +import { Logger } from "./l"; +import "./t2"; +Logger.apply; +console.log("t1 end"); diff --git a/test/regression/issue/06946/t2.js b/test/regression/issue/06946/t2.js new file mode 100644 index 0000000000..5e7521cc10 --- /dev/null +++ b/test/regression/issue/06946/t2.js @@ -0,0 +1,3 @@ +console.log("t2 begin"); +require("./t3"); +console.log("t2 end"); diff --git a/test/regression/issue/06946/t3.mjs b/test/regression/issue/06946/t3.mjs new file mode 100644 index 0000000000..bddd97b89f --- /dev/null +++ b/test/regression/issue/06946/t3.mjs @@ -0,0 +1,5 @@ +console.log("t3 begin"); +import { Logger } from "./l"; +console.log("t3 end"); +Logger.apply; +console.log("t3 postend"); diff --git a/test/regression/issue/08965/08965.test.ts b/test/regression/issue/08965/08965.test.ts new file mode 100644 index 0000000000..8de7985eda --- /dev/null +++ b/test/regression/issue/08965/08965.test.ts @@ -0,0 +1,19 @@ +import { test, expect } from "bun:test"; +import { bunEnv, bunExe } from "harness"; +import { join } from "path"; + +test("08965", async () => { + const buns = Array.from( + { length: 25 }, + () => + Bun.spawn({ + cmd: [bunExe(), join(import.meta.dir, "1.ts")], + cwd: import.meta.dir, + stdio: ["inherit", "inherit", "inherit"], + env: bunEnv, + }).exited, + ); + + const exited = await Promise.all(buns); + expect(exited).toEqual(Array.from({ length: 25 }, () => 0)); +}); diff --git a/test/regression/issue/08965/1.ts b/test/regression/issue/08965/1.ts new file mode 100644 index 0000000000..5e836325b4 --- /dev/null +++ b/test/regression/issue/08965/1.ts @@ -0,0 +1,8 @@ +import SomeClass from "./3"; + +import { config } from "./5"; + +const client = {}; + +console.log(SomeClass); +client.config = config; diff --git a/test/regression/issue/08965/3.ts b/test/regression/issue/08965/3.ts new file mode 100644 index 0000000000..da28065508 --- /dev/null +++ b/test/regression/issue/08965/3.ts @@ -0,0 +1,3 @@ +import somedata = require("./4"); + +export default class SomeClass {} diff --git a/test/regression/issue/08965/4.js b/test/regression/issue/08965/4.js new file mode 100644 index 0000000000..2e752cc4e0 --- /dev/null +++ b/test/regression/issue/08965/4.js @@ -0,0 +1,4 @@ +// 4.js +const config = require(`./5`); + +module.exports = {}; diff --git a/test/regression/issue/08965/5.ts b/test/regression/issue/08965/5.ts new file mode 100644 index 0000000000..ec923cf00b --- /dev/null +++ b/test/regression/issue/08965/5.ts @@ -0,0 +1,6 @@ +const config_ = {}; + +type GeneratedConfigType = typeof config_; +export interface Config extends GeneratedConfigType {} + +export const config = config_ as Config;