From 392212b0901e405a27a099737eae0c3eaf634aea Mon Sep 17 00:00:00 2001 From: Kai Tamkun <13513421+heimskr@users.noreply.github.com> Date: Fri, 23 May 2025 22:59:58 -0700 Subject: [PATCH] node:vm compatibility (#19703) --- cmake/Sources.json | 1 + cmake/sources/CxxSources.txt | 5 + cmake/targets/BuildBun.cmake | 4 +- src/bun.js/bindings/BunProcess.cpp | 4 +- src/bun.js/bindings/ErrorCode.cpp | 28 + src/bun.js/bindings/ErrorCode.ts | 9 +- src/bun.js/bindings/JSNextTickQueue.cpp | 1 + src/bun.js/bindings/NodeVM.cpp | 1932 +++++++---------- src/bun.js/bindings/NodeVM.h | 62 +- src/bun.js/bindings/NodeVMModule.cpp | 395 ++++ src/bun.js/bindings/NodeVMModule.h | 123 ++ src/bun.js/bindings/NodeVMScript.cpp | 646 ++++++ src/bun.js/bindings/NodeVMScript.h | 113 + .../bindings/NodeVMSourceTextModule.cpp | 461 ++++ src/bun.js/bindings/NodeVMSourceTextModule.h | 73 + src/bun.js/bindings/ZigGlobalObject.cpp | 48 +- src/bun.js/bindings/ZigGlobalObject.h | 18 +- .../bindings/webcore/DOMClientIsoSubspaces.h | 4 +- src/bun.js/bindings/webcore/DOMIsoSubspaces.h | 4 +- src/js/builtins.d.ts | 6 + src/js/builtins/BunBuiltinNames.h | 3 + src/js/internal/primordials.js | 23 + src/js/internal/validators.ts | 11 +- src/js/node/vm.ts | 409 +++- src/vm/Semaphore.cpp | 51 + src/vm/Semaphore.h | 34 + src/vm/SigintReceiver.h | 23 + src/vm/SigintWatcher.cpp | 208 ++ src/vm/SigintWatcher.h | 110 + test/js/node/test/parallel/test-vm-basic.js | 350 +++ .../parallel/test-vm-module-cached-data.js | 30 + .../test/parallel/test-vm-module-errors.js | 276 +++ .../node/test/parallel/test-vm-module-link.js | 168 ++ test/js/node/test/parallel/test-vm-sigint.js | 52 + .../test-vm-timeout-escape-promise-module.js | 42 + .../test-vm-timeout-escape-promise.js | 39 + test/js/node/test/parallel/test-vm-timeout.js | 81 + 37 files changed, 4611 insertions(+), 1236 deletions(-) create mode 100644 src/bun.js/bindings/NodeVMModule.cpp create mode 100644 src/bun.js/bindings/NodeVMModule.h create mode 100644 src/bun.js/bindings/NodeVMScript.cpp create mode 100644 src/bun.js/bindings/NodeVMScript.h create mode 100644 src/bun.js/bindings/NodeVMSourceTextModule.cpp create mode 100644 src/bun.js/bindings/NodeVMSourceTextModule.h create mode 100644 src/vm/Semaphore.cpp create mode 100644 src/vm/Semaphore.h create mode 100644 src/vm/SigintReceiver.h create mode 100644 src/vm/SigintWatcher.cpp create mode 100644 src/vm/SigintWatcher.h create mode 100644 test/js/node/test/parallel/test-vm-basic.js create mode 100644 test/js/node/test/parallel/test-vm-module-cached-data.js create mode 100644 test/js/node/test/parallel/test-vm-module-errors.js create mode 100644 test/js/node/test/parallel/test-vm-module-link.js create mode 100644 test/js/node/test/parallel/test-vm-sigint.js create mode 100644 test/js/node/test/parallel/test-vm-timeout-escape-promise-module.js create mode 100644 test/js/node/test/parallel/test-vm-timeout-escape-promise.js create mode 100644 test/js/node/test/parallel/test-vm-timeout.js diff --git a/cmake/Sources.json b/cmake/Sources.json index d8e6f3b117..ac6bd4c859 100644 --- a/cmake/Sources.json +++ b/cmake/Sources.json @@ -48,6 +48,7 @@ "src/bun.js/bindings/v8/shim/*.cpp", "src/bake/*.cpp", "src/deps/*.cpp", + "src/vm/*.cpp", "packages/bun-usockets/src/crypto/*.cpp" ] }, diff --git a/cmake/sources/CxxSources.txt b/cmake/sources/CxxSources.txt index 57283a4de3..318dd51ec9 100644 --- a/cmake/sources/CxxSources.txt +++ b/cmake/sources/CxxSources.txt @@ -153,6 +153,9 @@ src/bun.js/bindings/NodeTLS.cpp src/bun.js/bindings/NodeURL.cpp src/bun.js/bindings/NodeValidator.cpp src/bun.js/bindings/NodeVM.cpp +src/bun.js/bindings/NodeVMModule.cpp +src/bun.js/bindings/NodeVMScript.cpp +src/bun.js/bindings/NodeVMSourceTextModule.cpp src/bun.js/bindings/NoOpForTesting.cpp src/bun.js/bindings/ObjectBindings.cpp src/bun.js/bindings/objects.cpp @@ -466,3 +469,5 @@ src/bun.js/modules/NodeUtilTypesModule.cpp src/bun.js/modules/ObjectModule.cpp src/deps/libuwsockets.cpp src/io/io_darwin.cpp +src/vm/Semaphore.cpp +src/vm/SigintWatcher.cpp diff --git a/cmake/targets/BuildBun.cmake b/cmake/targets/BuildBun.cmake index 4a63ecf650..06e4b511e7 100644 --- a/cmake/targets/BuildBun.cmake +++ b/cmake/targets/BuildBun.cmake @@ -744,7 +744,7 @@ target_include_directories(${bun} PRIVATE ${NODEJS_HEADERS_PATH}/include ) -if(NOT WIN32) +if(NOT WIN32) target_include_directories(${bun} PRIVATE ${CWD}/src/bun.js/bindings/libuv) endif() @@ -877,7 +877,7 @@ if(NOT WIN32) -Wno-nullability-completeness -Werror ) - + if(ENABLE_ASAN) target_compile_options(${bun} PUBLIC -fsanitize=address diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index 4c58d1acca..61fe743270 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -875,8 +875,8 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionChdir, (JSC::JSGlobalObject * globalObj RELEASE_AND_RETURN(scope, JSC::JSValue::encode(result)); } -static HashMap* signalNameToNumberMap = nullptr; static HashMap* signalNumberToNameMap = nullptr; +static HashMap* signalNameToNumberMap = nullptr; // On windows, signals need to have a handle to the uv_signal_t. When sigaction is used, this is kept track globally for you. struct SignalHandleValue { @@ -1143,7 +1143,7 @@ static void onDidChangeListeners(EventEmitter& eventEmitter, const Identifier& e { if (Bun__isMainThreadVM()) { // IPC handlers - if (eventName.string() == "message"_s || eventName.string() == "disconnect"_s) { + if (eventName == "message" || eventName == "disconnect") { auto* global = jsCast(eventEmitter.scriptExecutionContext()->jsGlobalObject()); auto& vm = JSC::getVM(global); auto messageListenerCount = eventEmitter.listenerCount(vm.propertyNames->message); diff --git a/src/bun.js/bindings/ErrorCode.cpp b/src/bun.js/bindings/ErrorCode.cpp index 5df3e591ff..7bb307f0e7 100644 --- a/src/bun.js/bindings/ErrorCode.cpp +++ b/src/bun.js/bindings/ErrorCode.cpp @@ -2224,6 +2224,26 @@ JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_CHILD_PROCESS_STDIO_MAXBUFFER, message)); } + case Bun::ErrorCode::ERR_VM_MODULE_STATUS: { + auto arg0 = callFrame->argument(1); + auto str0 = arg0.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto message = makeString("Module status "_s, str0); + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_VM_MODULE_STATUS, message)); + } + + case Bun::ErrorCode::ERR_VM_MODULE_LINK_FAILURE: { + auto arg0 = callFrame->argument(1); + auto message = arg0.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto cause = callFrame->argument(2); + JSObject* error = createError(globalObject, ErrorCode::ERR_VM_MODULE_LINK_FAILURE, message); + RETURN_IF_EXCEPTION(scope, {}); + error->putDirect(vm, Identifier::fromString(vm, "cause"_s), cause); + RETURN_IF_EXCEPTION(scope, {}); + return JSC::JSValue::encode(error); + } + case ErrorCode::ERR_IPC_DISCONNECTED: return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_IPC_DISCONNECTED, "IPC channel is already disconnected"_s)); case ErrorCode::ERR_SERVER_NOT_RUNNING: @@ -2332,6 +2352,14 @@ JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_HTTP_SOCKET_ASSIGNED, "Socket already assigned"_s)); case ErrorCode::ERR_STREAM_RELEASE_LOCK: return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_STREAM_RELEASE_LOCK, "Stream reader cancelled via releaseLock()"_s)); + case ErrorCode::ERR_VM_MODULE_ALREADY_LINKED: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_VM_MODULE_ALREADY_LINKED, "Module has already been linked"_s)); + case ErrorCode::ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA, "Cached data cannot be created for a module which has been evaluated"_s)); + case ErrorCode::ERR_VM_MODULE_NOT_MODULE: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_VM_MODULE_NOT_MODULE, "Provided module is not an instance of Module"_s)); + case ErrorCode::ERR_VM_MODULE_DIFFERENT_CONTEXT: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_VM_MODULE_DIFFERENT_CONTEXT, "Linked modules must use the same context"_s)); default: { break; diff --git a/src/bun.js/bindings/ErrorCode.ts b/src/bun.js/bindings/ErrorCode.ts index 7a278efcc0..1f833272db 100644 --- a/src/bun.js/bindings/ErrorCode.ts +++ b/src/bun.js/bindings/ErrorCode.ts @@ -249,8 +249,6 @@ const errors: ErrorCodeMapping = [ ["ERR_UNKNOWN_ENCODING", TypeError], ["ERR_UNKNOWN_SIGNAL", TypeError], ["ERR_USE_AFTER_CLOSE", Error], - ["ERR_VM_MODULE_CACHED_DATA_REJECTED", Error], - ["ERR_VM_MODULE_LINK_FAILURE", Error], ["ERR_WASI_NOT_STARTED", Error], ["ERR_WORKER_INIT_FAILED", Error], ["ERR_WORKER_NOT_RUNNING", Error], @@ -283,6 +281,13 @@ const errors: ErrorCodeMapping = [ ["HPE_INVALID_EOF_STATE", Error], ["HPE_INVALID_METHOD", Error], ["HPE_INTERNAL", Error], + ["ERR_VM_MODULE_STATUS", Error], + ["ERR_VM_MODULE_ALREADY_LINKED", Error], + ["ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA", Error], + ["ERR_VM_MODULE_NOT_MODULE", Error], + ["ERR_VM_MODULE_DIFFERENT_CONTEXT", Error], + ["ERR_VM_MODULE_LINK_FAILURE", Error], + ["ERR_VM_MODULE_CACHED_DATA_REJECTED", Error], ["HPE_INVALID_HEADER_TOKEN", Error], ["HPE_HEADER_OVERFLOW", Error], ]; diff --git a/src/bun.js/bindings/JSNextTickQueue.cpp b/src/bun.js/bindings/JSNextTickQueue.cpp index 190b5714b5..baf1781882 100644 --- a/src/bun.js/bindings/JSNextTickQueue.cpp +++ b/src/bun.js/bindings/JSNextTickQueue.cpp @@ -89,6 +89,7 @@ void JSNextTickQueue::drain(JSC::VM& vm, JSC::JSGlobalObject* globalObject) RETURN_IF_EXCEPTION(throwScope, ); if (mustResetContext) { globalObject->m_asyncContextData.get()->putInternalField(vm, 0, jsUndefined()); + RETURN_IF_EXCEPTION(throwScope, ); } auto* drainFn = internalField(2).get().getObject(); MarkedArgumentBuffer drainArgs; diff --git a/src/bun.js/bindings/NodeVM.cpp b/src/bun.js/bindings/NodeVM.cpp index 1002de4582..d233549e62 100644 --- a/src/bun.js/bindings/NodeVM.cpp +++ b/src/bun.js/bindings/NodeVM.cpp @@ -9,6 +9,9 @@ #include "BunClientData.h" #include "NodeVM.h" +#include "NodeVMScript.h" +#include "NodeVMModule.h" +#include "NodeVMSourceTextModule.h" #include "JavaScriptCore/JSObjectInlines.h" #include "wtf/text/ExternalStringImpl.h" @@ -42,442 +45,414 @@ #include "NodeValidator.h" #include "JavaScriptCore/JSCInlines.h" -#include "JavaScriptCore/CodeCache.h" #include "JavaScriptCore/BytecodeCacheError.h" -#include "wtf/FileHandle.h" - +#include "JavaScriptCore/CodeCache.h" +#include "JavaScriptCore/FunctionCodeBlock.h" #include "JavaScriptCore/ProgramCodeBlock.h" #include "JavaScriptCore/JIT.h" +#include "wtf/FileHandle.h" + +#include "../vm/SigintWatcher.h" namespace Bun { using namespace WebCore; -/// For vm.compileFunction we need to return an anonymous function expression -/// -/// This code is adapted/inspired from JSC::constructFunction, which is used for function declarations. -static JSC::JSFunction* constructAnonymousFunction(JSC::JSGlobalObject* globalObject, const ArgList& args, const SourceOrigin& sourceOrigin, const String& fileName = String(), JSC::SourceTaintedOrigin sourceTaintOrigin = JSC::SourceTaintedOrigin::Untainted, TextPosition position = TextPosition(), JSC::JSScope* scope = nullptr); -static String stringifyAnonymousFunction(JSGlobalObject* globalObject, const ArgList& args, ThrowScope& scope, int* outOffset); +namespace NodeVM { -static RefPtr getBytecode(JSGlobalObject* globalObject, JSC::ProgramExecutable* executable, JSC::SourceCode source); -static JSC::EncodedJSValue createCachedData(JSGlobalObject* globalObject, JSC::SourceCode source); +bool extractCachedData(JSValue cachedDataValue, WTF::Vector& outCachedData) +{ + if (!cachedDataValue.isCell()) { + return false; + } -NodeVMGlobalObject* createContextImpl(JSC::VM& vm, JSGlobalObject* globalObject, JSObject* sandbox); + if (auto* arrayBufferView = JSC::jsDynamicCast(cachedDataValue)) { + if (!arrayBufferView->isDetached()) { + outCachedData = arrayBufferView->span(); + return true; + } + } else if (auto* arrayBuffer = JSC::jsDynamicCast(cachedDataValue); arrayBuffer && arrayBuffer->impl()) { + outCachedData = arrayBuffer->impl()->toVector(); + return true; + } -/// For some reason Node has this error message with a grammar error and we have to match it so the tests pass: + return false; +} + +JSC::JSFunction* constructAnonymousFunction(JSC::JSGlobalObject* globalObject, const ArgList& args, const SourceOrigin& sourceOrigin, CompileFunctionOptions&& options, JSC::SourceTaintedOrigin sourceTaintOrigin, JSC::JSScope* scope) +{ + ASSERT(scope); + + VM& vm = globalObject->vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); + + TextPosition position(options.lineOffset, options.columnOffset); + LexicallyScopedFeatures lexicallyScopedFeatures = globalObject->globalScopeExtension() ? TaintedByWithScopeLexicallyScopedFeature : NoLexicallyScopedFeatures; + + // First try parsing the code as is without wrapping it in an anonymous function expression. + // This is to reject cases where the user passes in a string like "});(function() {". + if (!args.isEmpty() && args.at(0).isString()) { + ParserError error; + String code = args.at(0).toWTFString(globalObject); + + SourceCode sourceCode( + JSC::StringSourceProvider::create(code, sourceOrigin, options.filename, sourceTaintOrigin, position, SourceProviderSourceType::Program), + position.m_line.oneBasedInt(), position.m_column.oneBasedInt()); + + if (!checkSyntax(vm, sourceCode, error)) { + ASSERT(error.isValid()); + + bool actuallyValid = true; + + if (error.type() == ParserError::ErrorType::SyntaxError && error.syntaxErrorType() == ParserError::SyntaxErrorIrrecoverable) { + String message = error.message(); + if (message == "Return statements are only valid inside functions.") { + actuallyValid = false; + } else { + const JSToken& token = error.token(); + int start = token.m_startPosition.offset; + int end = token.m_endPosition.offset; + if (start >= 0 && start < end) { + StringView tokenView = sourceCode.view().substring(start, end - start); + error = ParserError(ParserError::SyntaxError, ParserError::SyntaxErrorIrrecoverable, token, makeString("Unexpected token '"_s, tokenView, '\''), error.line()); + } + } + } + + if (actuallyValid) { + auto exception = error.toErrorObject(globalObject, sourceCode, -1); + throwException(globalObject, throwScope, exception); + return nullptr; + } + } + } + + // wrap the arguments in an anonymous function expression + int startOffset = 0; + String code = stringifyAnonymousFunction(globalObject, args, throwScope, &startOffset); + EXCEPTION_ASSERT(!!throwScope.exception() == code.isNull()); + + SourceCode sourceCode( + JSC::StringSourceProvider::create(code, sourceOrigin, WTFMove(options.filename), sourceTaintOrigin, position, SourceProviderSourceType::Program), + position.m_line.oneBasedInt(), position.m_column.oneBasedInt()); + + CodeCache* cache = vm.codeCache(); + ProgramExecutable* programExecutable = ProgramExecutable::create(globalObject, sourceCode); + + UnlinkedProgramCodeBlock* unlinkedProgramCodeBlock = nullptr; + RefPtr cachedBytecode; + + TriState bytecodeAccepted = TriState::Indeterminate; + + if (!options.cachedData.isEmpty()) { + cachedBytecode = CachedBytecode::create(std::span(options.cachedData), nullptr, {}); + SourceCodeKey key(sourceCode, {}, JSC::SourceCodeType::ProgramType, lexicallyScopedFeatures, JSC::JSParserScriptMode::Classic, JSC::DerivedContextType::None, JSC::EvalContextType::None, false, {}, std::nullopt); + unlinkedProgramCodeBlock = JSC::decodeCodeBlock(vm, key, *cachedBytecode); + if (unlinkedProgramCodeBlock == nullptr) { + bytecodeAccepted = TriState::False; + } else { + bytecodeAccepted = TriState::True; + } + } + + ParserError error; + + if (unlinkedProgramCodeBlock == nullptr) { + unlinkedProgramCodeBlock = cache->getUnlinkedProgramCodeBlock(vm, programExecutable, sourceCode, {}, error); + } + + if (!unlinkedProgramCodeBlock || error.isValid()) { + return nullptr; + } + + ProgramCodeBlock* programCodeBlock = nullptr; + { + DeferGC deferGC(vm); + programCodeBlock = ProgramCodeBlock::create(vm, programExecutable, unlinkedProgramCodeBlock, scope); + } + + if (!programCodeBlock || programCodeBlock->numberOfFunctionExprs() == 0) { + return nullptr; + } + + FunctionExecutable* functionExecutable = programCodeBlock->functionExpr(0); + if (!functionExecutable) { + return nullptr; + } + + Structure* structure = JSFunction::selectStructureForNewFuncExp(globalObject, functionExecutable); + JSFunction* function = JSFunction::create(vm, globalObject, functionExecutable, scope, structure); + + if (bytecodeAccepted == TriState::Indeterminate) { + if (options.produceCachedData) { + RefPtr producedBytecode = getBytecode(globalObject, programExecutable, sourceCode); + if (producedBytecode) { + JSC::JSUint8Array* buffer = WebCore::createBuffer(globalObject, producedBytecode->span()); + function->putDirect(vm, JSC::Identifier::fromString(vm, "cachedData"_s), buffer); + function->putDirect(vm, JSC::Identifier::fromString(vm, "cachedDataProduced"_s), jsBoolean(true)); + } else { + function->putDirect(vm, JSC::Identifier::fromString(vm, "cachedDataProduced"_s), jsBoolean(false)); + } + } + } else { + function->putDirect(vm, JSC::Identifier::fromString(vm, "cachedDataRejected"_s), jsBoolean(bytecodeAccepted == TriState::False)); + } + + return function; +} + +// Helper function to create an anonymous function expression with parameters +String stringifyAnonymousFunction(JSGlobalObject* globalObject, const ArgList& args, ThrowScope& scope, int* outOffset) +{ + // How we stringify functions is important for creating anonymous function expressions + String program; + if (args.isEmpty()) { + // No arguments, just an empty function body + program = "(function () {\n\n})"_s; + } else if (args.size() == 1) { + // Just the function body + auto body = args.at(0).toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + program = tryMakeString("(function () {\n"_s, body, "\n})"_s); + *outOffset = "(function () {\n"_s.length(); + + if (!program) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return {}; + } + } else { + // Process parameters and body + unsigned parameterCount = args.size() - 1; + StringBuilder paramString; + + for (unsigned i = 0; i < parameterCount; ++i) { + auto param = args.at(i).toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + if (i > 0) { + paramString.append(", "_s); + } + + paramString.append(param); + } + + auto body = args.at(parameterCount).toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + program = tryMakeString("(function ("_s, paramString.toString(), ") {\n"_s, body, "\n})"_s); + *outOffset = "(function ("_s.length() + paramString.length() + ") {\n"_s.length(); + + if (!program) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return {}; + } + } + + return program; +} + +RefPtr getBytecode(JSGlobalObject* globalObject, JSC::ProgramExecutable* executable, const JSC::SourceCode& source) +{ + VM& vm = JSC::getVM(globalObject); + JSC::CodeCache* cache = vm.codeCache(); + JSC::ParserError parserError; + JSC::UnlinkedProgramCodeBlock* unlinked = cache->getUnlinkedProgramCodeBlock(vm, executable, source, {}, parserError); + if (!unlinked || parserError.isValid()) { + return nullptr; + } + JSC::LexicallyScopedFeatures lexicallyScopedFeatures = globalObject->globalScopeExtension() ? TaintedByWithScopeLexicallyScopedFeature : NoLexicallyScopedFeatures; + JSC::BytecodeCacheError bytecodeCacheError; + FileSystem::FileHandle fileHandle; + return JSC::serializeBytecode(vm, unlinked, source, JSC::SourceCodeType::ProgramType, lexicallyScopedFeatures, JSParserScriptMode::Classic, fileHandle, bytecodeCacheError, {}); +} + +RefPtr getBytecode(JSGlobalObject* globalObject, JSC::ModuleProgramExecutable* executable, const JSC::SourceCode& source) +{ + VM& vm = JSC::getVM(globalObject); + JSC::CodeCache* cache = vm.codeCache(); + JSC::ParserError parserError; + JSC::UnlinkedModuleProgramCodeBlock* unlinked = cache->getUnlinkedModuleProgramCodeBlock(vm, executable, source, {}, parserError); + if (!unlinked || parserError.isValid()) { + return nullptr; + } + JSC::LexicallyScopedFeatures lexicallyScopedFeatures = globalObject->globalScopeExtension() ? TaintedByWithScopeLexicallyScopedFeature : NoLexicallyScopedFeatures; + JSC::BytecodeCacheError bytecodeCacheError; + FileSystem::FileHandle fileHandle; + return JSC::serializeBytecode(vm, unlinked, source, JSC::SourceCodeType::ProgramType, lexicallyScopedFeatures, JSParserScriptMode::Classic, fileHandle, bytecodeCacheError, {}); +} + +JSC::EncodedJSValue createCachedData(JSGlobalObject* globalObject, const JSC::SourceCode& source) +{ + VM& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSC::ProgramExecutable* executable = JSC::ProgramExecutable::create(globalObject, source); + RETURN_IF_EXCEPTION(scope, {}); + + RefPtr bytecode = getBytecode(globalObject, executable, source); + RETURN_IF_EXCEPTION(scope, {}); + + if (!bytecode) [[unlikely]] { + return throwVMError(globalObject, scope, "createCachedData failed"_s); + } + + std::span bytes = bytecode->span(); + JSC::JSUint8Array* buffer = WebCore::createBuffer(globalObject, bytes); + + RETURN_IF_EXCEPTION(scope, {}); + ASSERT(buffer); + + return JSValue::encode(buffer); +} + +bool handleException(JSGlobalObject* globalObject, VM& vm, NakedPtr exception, ThrowScope& throwScope) +{ + if (auto* errorInstance = jsDynamicCast(exception->value())) { + errorInstance->materializeErrorInfoIfNeeded(vm, vm.propertyNames->stack); + RETURN_IF_EXCEPTION(throwScope, {}); + JSValue stack_jsval = errorInstance->get(globalObject, vm.propertyNames->stack); + RETURN_IF_EXCEPTION(throwScope, {}); + if (!stack_jsval.isString()) { + return false; + } + String stack = stack_jsval.toWTFString(globalObject); + + auto& e_stack = exception->stack(); + size_t stack_size = e_stack.size(); + if (stack_size == 0) { + return false; + } + auto& stack_frame = e_stack[0]; + auto source_url = stack_frame.sourceURL(vm); + if (source_url.isEmpty()) { + // copy what Node does: https://github.com/nodejs/node/blob/afe3909483a2d5ae6b847055f544da40571fb28d/lib/vm.js#L94 + source_url = "evalmachine."_s; + } + auto line_and_column = stack_frame.computeLineAndColumn(); + + String prepend = makeString(source_url, ":"_s, line_and_column.line, "\n"_s, stack); + errorInstance->putDirect(vm, vm.propertyNames->stack, jsString(vm, prepend), JSC::PropertyAttribute::DontEnum | 0); + + JSC::throwException(globalObject, throwScope, exception.get()); + return true; + } + return false; +} + +// Returns an encoded exception if the options are invalid. +// Otherwise, returns an empty optional. +std::optional getNodeVMContextOptions(JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSValue optionsArg, NodeVMContextOptions& outOptions, ASCIILiteral codeGenerationKey) +{ + outOptions = {}; + + // If options is provided, validate name and origin properties + if (!optionsArg.isObject()) { + return std::nullopt; + } + + JSObject* options = asObject(optionsArg); + + // Check name property + if (JSValue nameValue = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "name"_s))) { + RETURN_IF_EXCEPTION(scope, {}); + if (!nameValue.isUndefined() && !nameValue.isString()) { + return ERR::INVALID_ARG_TYPE(scope, globalObject, "options.name"_s, "string"_s, nameValue); + } + } + + // Check origin property + if (JSValue originValue = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "origin"_s))) { + RETURN_IF_EXCEPTION(scope, {}); + if (!originValue.isUndefined() && !originValue.isString()) { + return ERR::INVALID_ARG_TYPE(scope, globalObject, "options.origin"_s, "string"_s, originValue); + } + } + + if (JSValue codeGenerationValue = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, codeGenerationKey))) { + RETURN_IF_EXCEPTION(scope, {}); + + if (codeGenerationValue.isUndefined()) { + return std::nullopt; + } + + if (!codeGenerationValue.isObject()) { + return ERR::INVALID_ARG_TYPE(scope, globalObject, WTF::makeString("options."_s, codeGenerationKey), "object"_s, codeGenerationValue); + } + + JSObject* codeGenerationObject = asObject(codeGenerationValue); + + if (JSValue allowStringsValue = codeGenerationObject->getIfPropertyExists(globalObject, Identifier::fromString(vm, "strings"_s))) { + RETURN_IF_EXCEPTION(scope, {}); + if (!allowStringsValue.isBoolean()) { + return ERR::INVALID_ARG_TYPE(scope, globalObject, WTF::makeString("options."_s, codeGenerationKey, ".strings"_s), "boolean"_s, allowStringsValue); + } + + outOptions.allowStrings = allowStringsValue.toBoolean(globalObject); + } + + if (JSValue allowWasmValue = codeGenerationObject->getIfPropertyExists(globalObject, Identifier::fromString(vm, "wasm"_s))) { + RETURN_IF_EXCEPTION(scope, {}); + if (!allowWasmValue.isBoolean()) { + return ERR::INVALID_ARG_TYPE(scope, globalObject, WTF::makeString("options."_s, codeGenerationKey, ".wasm"_s), "boolean"_s, allowWasmValue); + } + + outOptions.allowWasm = allowWasmValue.toBoolean(globalObject); + } + } + + return std::nullopt; +} + +NodeVMGlobalObject* getGlobalObjectFromContext(JSGlobalObject* globalObject, JSValue contextValue, bool canThrow) +{ + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + + if (contextValue.isUndefinedOrNull()) { + if (canThrow) { + ERR::INVALID_ARG_TYPE(scope, globalObject, "context"_s, "object"_s, contextValue); + } + return nullptr; + } + + if (!contextValue.isObject()) { + if (canThrow) { + ERR::INVALID_ARG_TYPE(scope, globalObject, "context"_s, "object"_s, contextValue); + } + return nullptr; + } + + JSObject* context = asObject(contextValue); + auto* zigGlobalObject = defaultGlobalObject(globalObject); + JSValue scopeValue = zigGlobalObject->vmModuleContextMap()->get(context); + if (scopeValue.isUndefined()) { + if (canThrow) { + INVALID_ARG_VALUE_VM_VARIATION(scope, globalObject, "contextifiedObject"_s, context); + } + return nullptr; + } + + NodeVMGlobalObject* nodeVmGlobalObject = jsDynamicCast(scopeValue); + if (!nodeVmGlobalObject) { + if (canThrow) { + INVALID_ARG_VALUE_VM_VARIATION(scope, globalObject, "contextifiedObject"_s, context); + } + return nullptr; + } + + return nodeVmGlobalObject; +} + +/// For some reason Node has this error message with a grammatical error and we have to match it so the tests pass: /// `The "" argument must be an vm.Context` JSC::EncodedJSValue INVALID_ARG_VALUE_VM_VARIATION(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral name, JSC::JSValue value) { - WTF::StringBuilder builder; - builder.append("The \""_s); - builder.append(name); - builder.append("\" argument must be an vm.Context"_s); - - throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_INVALID_ARG_TYPE, builder.toString())); + throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_INVALID_ARG_TYPE, makeString("The \""_s, name, "\" argument must be an vm.Context"_s))); return {}; } -class BaseOptions { -public: - String filename = String(); - OrdinalNumber lineOffset; - OrdinalNumber columnOffset; - bool failed; +} // namespace NodeVM - bool fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg) - { - JSObject* options = nullptr; - bool any = false; - - if (!optionsArg.isUndefined()) { - if (optionsArg.isObject()) { - options = asObject(optionsArg); - } else { - auto _ = ERR::INVALID_ARG_TYPE(scope, globalObject, "options"_s, "object"_s, optionsArg); - return false; - } - - if (JSValue filenameOpt = options->getIfPropertyExists(globalObject, builtinNames(vm).filenamePublicName())) { - if (filenameOpt.isString()) { - this->filename = filenameOpt.toWTFString(globalObject); - RETURN_IF_EXCEPTION(scope, false); - any = true; - } else if (!filenameOpt.isUndefined()) { - ERR::INVALID_ARG_TYPE(scope, globalObject, "options.filename"_s, "string"_s, filenameOpt); - return false; - } - } else { - this->filename = "evalmachine."_s; - } - - if (JSValue lineOffsetOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "lineOffset"_s))) { - if (lineOffsetOpt.isAnyInt()) { - if (!lineOffsetOpt.isInt32()) { - ERR::OUT_OF_RANGE(scope, globalObject, "options.lineOffset"_s, std::numeric_limits().min(), std::numeric_limits().max(), lineOffsetOpt); - return false; - } - this->lineOffset = OrdinalNumber::fromZeroBasedInt(lineOffsetOpt.asInt32()); - any = true; - } else if (lineOffsetOpt.isNumber()) { - ERR::OUT_OF_RANGE(scope, globalObject, "options.lineOffset"_s, "an integer"_s, lineOffsetOpt); - return false; - } else if (!lineOffsetOpt.isUndefined()) { - ERR::INVALID_ARG_TYPE(scope, globalObject, "options.lineOffset"_s, "number"_s, lineOffsetOpt); - return false; - } - } - - if (JSValue columnOffsetOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "columnOffset"_s))) { - if (columnOffsetOpt.isAnyInt()) { - if (!columnOffsetOpt.isInt32()) { - ERR::OUT_OF_RANGE(scope, globalObject, "options.columnOffset"_s, std::numeric_limits().min(), std::numeric_limits().max(), columnOffsetOpt); - return false; - } - int columnOffsetValue = columnOffsetOpt.asInt32(); - - this->columnOffset = OrdinalNumber::fromZeroBasedInt(columnOffsetValue); - any = true; - } else if (columnOffsetOpt.isNumber()) { - ERR::OUT_OF_RANGE(scope, globalObject, "options.columnOffset"_s, "an integer"_s, columnOffsetOpt); - return false; - } else if (!columnOffsetOpt.isUndefined()) { - ERR::INVALID_ARG_TYPE(scope, globalObject, "options.columnOffset"_s, "number"_s, columnOffsetOpt); - return false; - } - } - } - - return any; - } - - bool validateProduceCachedData(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSObject* options, bool& outProduceCachedData) - { - JSValue produceCachedDataOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "produceCachedData"_s)); - if (produceCachedDataOpt && !produceCachedDataOpt.isUndefined()) { - RETURN_IF_EXCEPTION(scope, {}); - if (!produceCachedDataOpt.isBoolean()) { - ERR::INVALID_ARG_TYPE(scope, globalObject, "options.produceCachedData"_s, "boolean"_s, produceCachedDataOpt); - return false; - } - outProduceCachedData = produceCachedDataOpt.asBoolean(); - return true; - } - return false; - } - - bool validateCachedData(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSObject* options, std::vector& outCachedData) - { - JSValue cachedDataOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "cachedData"_s)); - if (cachedDataOpt && !cachedDataOpt.isUndefined()) { - RETURN_IF_EXCEPTION(scope, {}); - if (!cachedDataOpt.isCell()) { - ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.cachedData"_s, "Buffer, TypedArray, or DataView"_s, cachedDataOpt); - return false; - } - - // If it's a cell, verify it's a Buffer, TypedArray or DataView - if (cachedDataOpt.isCell()) { - bool isValidType = false; - - if (auto* arrayBufferView = JSC::jsDynamicCast(cachedDataOpt)) { - if (!arrayBufferView->isDetached()) { - std::span span = arrayBufferView->span(); - outCachedData = { span.begin(), span.end() }; - isValidType = true; - } - } else if (auto* arrayBuffer = JSC::jsDynamicCast(cachedDataOpt); arrayBuffer && arrayBuffer->impl()) { - std::span span = arrayBuffer->impl()->span(); - outCachedData = { span.begin(), span.end() }; - isValidType = true; - } - - if (!isValidType) { - ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.cachedData"_s, "Buffer, TypedArray, or DataView"_s, cachedDataOpt); - return false; - } - - return true; - } - } - return false; - } - - bool validateTimeout(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSObject* options, std::optional& outTimeout) - { - JSValue timeoutOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "timeout"_s)); - if (timeoutOpt && !timeoutOpt.isUndefined()) { - if (!timeoutOpt.isNumber()) { - ERR::INVALID_ARG_TYPE(scope, globalObject, "options.timeout"_s, "number"_s, timeoutOpt); - return false; - } - - ssize_t timeoutValue; - V::validateInteger(scope, globalObject, timeoutOpt, "options.timeout"_s, jsNumber(1), jsNumber(std::numeric_limits().max()), &timeoutValue); - RETURN_IF_EXCEPTION(scope, {}); - - outTimeout = timeoutValue; - return true; - } - return false; - } -}; - -class ScriptOptions : public BaseOptions { -public: - std::optional timeout = std::nullopt; - bool produceCachedData = false; - std::vector cachedData; - - bool fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg) - { - bool any = BaseOptions::fromJS(globalObject, vm, scope, optionsArg); - RETURN_IF_EXCEPTION(scope, false); - - if (!optionsArg.isUndefined() && !optionsArg.isString()) { - JSObject* options = asObject(optionsArg); - - // Validate contextName and contextOrigin are strings - if (JSValue contextNameOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "contextName"_s))) { - if (!contextNameOpt.isUndefined() && !contextNameOpt.isString()) { - ERR::INVALID_ARG_TYPE(scope, globalObject, "options.contextName"_s, "string"_s, contextNameOpt); - return false; - } - any = true; - } - RETURN_IF_EXCEPTION(scope, false); - - if (JSValue contextOriginOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "contextOrigin"_s))) { - if (!contextOriginOpt.isUndefined() && !contextOriginOpt.isString()) { - ERR::INVALID_ARG_TYPE(scope, globalObject, "options.contextOrigin"_s, "string"_s, contextOriginOpt); - return false; - } - any = true; - } - RETURN_IF_EXCEPTION(scope, false); - - if (validateTimeout(globalObject, vm, scope, options, this->timeout)) { - RETURN_IF_EXCEPTION(scope, false); - any = true; - } - - if (validateProduceCachedData(globalObject, vm, scope, options, this->produceCachedData)) { - RETURN_IF_EXCEPTION(scope, false); - any = true; - } - - if (validateCachedData(globalObject, vm, scope, options, this->cachedData)) { - RETURN_IF_EXCEPTION(scope, false); - any = true; - } - } - - return any; - } -}; - -class RunningScriptOptions : public BaseOptions { -public: - bool displayErrors = true; - std::optional timeout = std::nullopt; - bool breakOnSigint = false; - - bool fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg) - { - bool any = BaseOptions::fromJS(globalObject, vm, scope, optionsArg); - RETURN_IF_EXCEPTION(scope, false); - - if (!optionsArg.isUndefined() && !optionsArg.isString()) { - JSObject* options = asObject(optionsArg); - - if (JSValue displayErrorsOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "displayErrors"_s))) { - RETURN_IF_EXCEPTION(scope, false); - if (!displayErrorsOpt.isBoolean()) { - ERR::INVALID_ARG_TYPE(scope, globalObject, "options.displayErrors"_s, "boolean"_s, displayErrorsOpt); - return false; - } - this->displayErrors = displayErrorsOpt.asBoolean(); - any = true; - } - - if (validateTimeout(globalObject, vm, scope, options, this->timeout)) { - RETURN_IF_EXCEPTION(scope, false); - any = true; - } - - if (JSValue breakOnSigintOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "breakOnSigint"_s))) { - RETURN_IF_EXCEPTION(scope, false); - if (!breakOnSigintOpt.isBoolean()) { - ERR::INVALID_ARG_TYPE(scope, globalObject, "options.breakOnSigint"_s, "boolean"_s, breakOnSigintOpt); - return false; - } - this->breakOnSigint = breakOnSigintOpt.asBoolean(); - any = true; - } - } - - return any; - } -}; - -class CompileFunctionOptions : public BaseOptions { -public: - std::vector cachedData; - JSGlobalObject* parsingContext = nullptr; - JSValue contextExtensions; - bool produceCachedData = false; - - bool fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg) - { - this->parsingContext = globalObject; - bool any = BaseOptions::fromJS(globalObject, vm, scope, optionsArg); - RETURN_IF_EXCEPTION(scope, false); - - if (!optionsArg.isUndefined() && !optionsArg.isString()) { - JSObject* options = asObject(optionsArg); - - if (validateProduceCachedData(globalObject, vm, scope, options, this->produceCachedData)) { - RETURN_IF_EXCEPTION(scope, false); - any = true; - } - - if (validateCachedData(globalObject, vm, scope, options, this->cachedData)) { - RETURN_IF_EXCEPTION(scope, false); - any = true; - } - - JSValue parsingContextValue = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "parsingContext"_s)); - RETURN_IF_EXCEPTION(scope, {}); - - if (!parsingContextValue.isEmpty() && !parsingContextValue.isUndefined()) { - if (parsingContextValue.isNull() || !parsingContextValue.isObject()) - return ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.parsingContext"_s, "Context"_s, parsingContextValue); - - JSObject* context = asObject(parsingContextValue); - auto* zigGlobalObject = defaultGlobalObject(globalObject); - JSValue scopeValue = zigGlobalObject->vmModuleContextMap()->get(context); - - if (scopeValue.isUndefined()) - return ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.parsingContext"_s, "Context"_s, parsingContextValue); - - parsingContext = jsDynamicCast(scopeValue); - if (!parsingContext) - return ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.parsingContext"_s, "Context"_s, parsingContextValue); - - any = true; - } - - // Handle contextExtensions option - JSValue contextExtensionsValue = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "contextExtensions"_s)); - RETURN_IF_EXCEPTION(scope, {}); - - if (!contextExtensionsValue.isEmpty() && !contextExtensionsValue.isUndefined()) { - if (contextExtensionsValue.isNull() || !contextExtensionsValue.isObject()) - return ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.contextExtensions"_s, "Array"_s, contextExtensionsValue); - - if (auto* contextExtensionsObject = asObject(contextExtensionsValue)) { - if (!isArray(globalObject, contextExtensionsObject)) - return ERR::INVALID_ARG_TYPE(scope, globalObject, "options.contextExtensions"_s, "Array"_s, contextExtensionsValue); - - // Validate that all items in the array are objects - auto* contextExtensionsArray = jsCast(contextExtensionsValue); - unsigned length = contextExtensionsArray->length(); - for (unsigned i = 0; i < length; i++) { - JSValue extension = contextExtensionsArray->getIndexQuickly(i); - if (!extension.isObject()) - return ERR::INVALID_ARG_TYPE(scope, globalObject, "options.contextExtensions[0]"_s, "object"_s, extension); - } - } else { - return ERR::INVALID_ARG_TYPE(scope, globalObject, "options.contextExtensions"_s, "Array"_s, contextExtensionsValue); - } - - this->contextExtensions = contextExtensionsValue; - any = true; - } - } - - return any; - } -}; - -class NodeVMScriptConstructor final : public JSC::InternalFunction { -public: - using Base = JSC::InternalFunction; - - static NodeVMScriptConstructor* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, JSC::JSObject* prototype); - - DECLARE_EXPORT_INFO; - - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::InternalFunctionType, Base::StructureFlags), info()); - } - -private: - NodeVMScriptConstructor(JSC::VM& vm, JSC::Structure* structure); - - void finishCreation(JSC::VM&, JSC::JSObject* prototype); -}; -STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(NodeVMScriptConstructor, JSC::InternalFunction); - -class NodeVMScript final : public JSC::JSDestructibleObject { -public: - using Base = JSC::JSDestructibleObject; - - static NodeVMScript* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, JSC::SourceCode source, ScriptOptions options); - - DECLARE_EXPORT_INFO; - template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - return WebCore::subspaceForImpl( - vm, - [](auto& spaces) { return spaces.m_clientSubspaceForNodeVMScript.get(); }, - [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForNodeVMScript = std::forward(space); }, - [](auto& spaces) { return spaces.m_subspaceForNodeVMScript.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForNodeVMScript = std::forward(space); }); - } - - static void destroy(JSC::JSCell*); - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); - } - - static JSObject* createPrototype(VM& vm, JSGlobalObject* globalObject); - - JSC::ProgramExecutable* createExecutable(); - void cacheBytecode(); - JSC::JSUint8Array* getBytecodeBuffer(); - - const JSC::SourceCode& source() const { return m_source; } - std::vector& cachedData() { return m_options.cachedData; } - RefPtr cachedBytecode() const { return m_cachedBytecode; } - JSC::ProgramExecutable* cachedExecutable() const { return m_cachedExecutable.get(); } - bool cachedDataProduced() const { return m_cachedDataProduced; } - void cachedDataProduced(bool value) { m_cachedDataProduced = value; } - TriState cachedDataRejected() const { return m_cachedDataRejected; } - void cachedDataRejected(TriState value) { m_cachedDataRejected = value; } - - DECLARE_VISIT_CHILDREN; - -private: - JSC::SourceCode m_source; - RefPtr m_cachedBytecode; - mutable JSC::WriteBarrier m_cachedBytecodeBuffer; - mutable JSC::WriteBarrier m_cachedExecutable; - ScriptOptions m_options; - bool m_cachedDataProduced = false; - TriState m_cachedDataRejected = TriState::Indeterminate; - - NodeVMScript(JSC::VM& vm, JSC::Structure* structure, JSC::SourceCode source, ScriptOptions options) - : Base(vm, structure) - , m_source(source) - , m_options(WTFMove(options)) - { - } - - void finishCreation(JSC::VM&); -}; +using namespace NodeVM; NodeVMGlobalObject::NodeVMGlobalObject(JSC::VM& vm, JSC::Structure* structure) : Base(vm, structure) @@ -497,10 +472,10 @@ template JSC::GCClient::IsoSubspace* NodeVMG [](auto& server) -> JSC::HeapCellType& { return server.m_heapCellTypeForNodeVMGlobalObject; }); } -NodeVMGlobalObject* NodeVMGlobalObject::create(JSC::VM& vm, JSC::Structure* structure) +NodeVMGlobalObject* NodeVMGlobalObject::create(JSC::VM& vm, JSC::Structure* structure, NodeVMContextOptions options) { auto* cell = new (NotNull, JSC::allocateCell(vm)) NodeVMGlobalObject(vm, structure); - cell->finishCreation(vm); + cell->finishCreation(vm, options); return cell; } @@ -510,9 +485,12 @@ Structure* NodeVMGlobalObject::createStructure(JSC::VM& vm, JSC::JSValue prototy return JSC::Structure::create(vm, nullptr, prototype, JSC::TypeInfo(JSC::GlobalObjectType, StructureFlags & ~IsImmutablePrototypeExoticObject), info()); } -void NodeVMGlobalObject::finishCreation(JSC::VM&) +void NodeVMGlobalObject::finishCreation(JSC::VM& vm, NodeVMContextOptions options) { - Base::finishCreation(vm()); + Base::finishCreation(vm); + setEvalEnabled(options.allowStrings, "Code generation from strings disallowed for this context"_s); + setWebAssemblyEnabled(options.allowWasm, "Wasm code generation disallowed by embedder"_s); + vm.ensureTerminationException(); } void NodeVMGlobalObject::destroy(JSCell* cell) @@ -522,6 +500,7 @@ void NodeVMGlobalObject::destroy(JSCell* cell) NodeVMGlobalObject::~NodeVMGlobalObject() { + SigintWatcher::get().unregisterGlobalObject(this); } void NodeVMGlobalObject::setContextifiedObject(JSC::JSObject* contextifiedObject) @@ -534,10 +513,13 @@ void NodeVMGlobalObject::clearContextifiedObject() m_sandbox.clear(); } +void NodeVMGlobalObject::sigintReceived() +{ + vm().notifyNeedTermination(); +} + bool NodeVMGlobalObject::put(JSCell* cell, JSGlobalObject* globalObject, PropertyName propertyName, JSValue value, PutPropertySlot& slot) { - // if (!propertyName.isSymbol()) - // printf("put called for %s\n", propertyName.publicName()->utf8().data()); auto* thisObject = jsCast(cell); if (!thisObject->m_sandbox) { @@ -546,10 +528,12 @@ bool NodeVMGlobalObject::put(JSCell* cell, JSGlobalObject* globalObject, Propert auto* sandbox = thisObject->m_sandbox.get(); - auto& vm = JSC::getVM(globalObject); + VM& vm = JSC::getVM(globalObject); JSValue thisValue = slot.thisValue(); bool isContextualStore = thisValue != JSValue(globalObject); - (void)isContextualStore; + if (auto* proxy = jsDynamicCast(thisValue); proxy && proxy->target() == globalObject) { + isContextualStore = false; + } bool isDeclaredOnGlobalObject = slot.type() == JSC::PutPropertySlot::NewProperty; auto scope = DECLARE_THROW_SCOPE(vm); PropertySlot getter(sandbox, PropertySlot::InternalMethodType::Get, nullptr); @@ -588,10 +572,7 @@ static const ASCIILiteral s_proxyAlreadyRevokedErrorMessage { "Proxy has already bool NodeVMGlobalObject::getOwnPropertySlot(JSObject* cell, JSGlobalObject* globalObject, PropertyName propertyName, PropertySlot& slot) { - // if (!propertyName.isSymbol()) - // printf("getOwnPropertySlot called for %s\n", propertyName.publicName()->utf8().data()); - - auto& vm = JSC::getVM(globalObject); + VM& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); auto* thisObject = jsCast(cell); @@ -686,7 +667,7 @@ bool NodeVMGlobalObject::defineOwnProperty(JSObject* cell, JSGlobalObject* globa } auto* contextifiedObject = thisObject->m_sandbox.get(); - auto& vm = JSC::getVM(globalObject); + VM& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); PropertySlot slot(globalObject, PropertySlot::InternalMethodType::GetOwnProperty, nullptr); @@ -726,325 +707,6 @@ void NodeVMGlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) visitor.append(thisObject->m_sandbox); } -static EncodedJSValue -constructScript(JSGlobalObject* globalObject, CallFrame* callFrame, JSValue newTarget = JSValue()) -{ - VM& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - ArgList args(callFrame); - JSValue sourceArg = args.at(0); - String sourceString = sourceArg.isUndefined() ? emptyString() : sourceArg.toWTFString(globalObject); - RETURN_IF_EXCEPTION(scope, encodedJSUndefined()); - - JSValue optionsArg = args.at(1); - ScriptOptions options; - if (optionsArg.isString()) { - options.filename = optionsArg.toWTFString(globalObject); - RETURN_IF_EXCEPTION(scope, {}); - } else if (!options.fromJS(globalObject, vm, scope, optionsArg)) { - RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined())); - options = {}; - } - - auto* zigGlobalObject = defaultGlobalObject(globalObject); - Structure* structure = zigGlobalObject->NodeVMScriptStructure(); - if (zigGlobalObject->NodeVMScript() != newTarget) [[unlikely]] { - auto scope = DECLARE_THROW_SCOPE(vm); - if (!newTarget) { - throwTypeError(globalObject, scope, "Class constructor Script cannot be invoked without 'new'"_s); - return {}; - } - - auto* functionGlobalObject = defaultGlobalObject(getFunctionRealm(globalObject, newTarget.getObject())); - RETURN_IF_EXCEPTION(scope, {}); - structure = InternalFunction::createSubclassStructure( - globalObject, newTarget.getObject(), functionGlobalObject->NodeVMScriptStructure()); - scope.release(); - } - - SourceCode source( - JSC::StringSourceProvider::create(sourceString, JSC::SourceOrigin(WTF::URL::fileURLWithFileSystemPath(options.filename)), options.filename, JSC::SourceTaintedOrigin::Untainted, TextPosition(options.lineOffset, options.columnOffset)), - options.lineOffset.zeroBasedInt(), options.columnOffset.zeroBasedInt()); - RETURN_IF_EXCEPTION(scope, {}); - - const bool produceCachedData = options.produceCachedData; - auto filename = options.filename; - - NodeVMScript* script = NodeVMScript::create(vm, globalObject, structure, source, WTFMove(options)); - - std::vector& cachedData = script->cachedData(); - - if (!cachedData.empty()) { - JSC::ProgramExecutable* executable = script->cachedExecutable(); - if (!executable) { - executable = script->createExecutable(); - } - ASSERT(executable); - - JSC::LexicallyScopedFeatures lexicallyScopedFeatures = globalObject->globalScopeExtension() ? JSC::TaintedByWithScopeLexicallyScopedFeature : JSC::NoLexicallyScopedFeatures; - JSC::SourceCodeKey key(source, {}, JSC::SourceCodeType::ProgramType, lexicallyScopedFeatures, JSC::JSParserScriptMode::Classic, JSC::DerivedContextType::None, JSC::EvalContextType::None, false, {}, std::nullopt); - Ref cachedBytecode = JSC::CachedBytecode::create(std::span(cachedData), nullptr, {}); - JSC::UnlinkedProgramCodeBlock* unlinkedBlock = JSC::decodeCodeBlock(vm, key, WTFMove(cachedBytecode)); - - if (!unlinkedBlock) { - script->cachedDataRejected(TriState::True); - } else { - JSC::JSScope* jsScope = globalObject->globalScope(); - JSC::CodeBlock* codeBlock = nullptr; - { - // JSC::ProgramCodeBlock::create() requires GC to be deferred. - DeferGC deferGC(vm); - codeBlock = JSC::ProgramCodeBlock::create(vm, executable, unlinkedBlock, jsScope); - } - JSC::CompilationResult compilationResult = JIT::compileSync(vm, codeBlock, JITCompilationEffort::JITCompilationCanFail); - if (compilationResult != JSC::CompilationResult::CompilationFailed) { - executable->installCode(codeBlock); - script->cachedDataRejected(TriState::False); - } else { - script->cachedDataRejected(TriState::True); - } - } - } else if (produceCachedData) { - script->cacheBytecode(); - // TODO(@heimskr): is there ever a case where bytecode production fails? - script->cachedDataProduced(true); - } - - return JSValue::encode(script); -} - -static bool handleException(JSGlobalObject* globalObject, VM& vm, NakedPtr exception, ThrowScope& throwScope) -{ - if (auto* errorInstance = jsDynamicCast(exception->value())) { - errorInstance->materializeErrorInfoIfNeeded(vm, vm.propertyNames->stack); - RETURN_IF_EXCEPTION(throwScope, {}); - JSValue stack_jsval = errorInstance->get(globalObject, vm.propertyNames->stack); - RETURN_IF_EXCEPTION(throwScope, {}); - if (!stack_jsval.isString()) { - return false; - } - String stack = stack_jsval.toWTFString(globalObject); - - auto& e_stack = exception->stack(); - size_t stack_size = e_stack.size(); - if (stack_size == 0) { - return false; - } - auto& stack_frame = e_stack[0]; - auto source_url = stack_frame.sourceURL(vm); - if (source_url.isEmpty()) { - // copy what Node does: https://github.com/nodejs/node/blob/afe3909483a2d5ae6b847055f544da40571fb28d/lib/vm.js#L94 - source_url = "evalmachine."_s; - } - auto line_and_column = stack_frame.computeLineAndColumn(); - - String prepend = makeString(source_url, ":"_s, line_and_column.line, "\n"_s, stack); - errorInstance->putDirect(vm, vm.propertyNames->stack, jsString(vm, prepend), JSC::PropertyAttribute::DontEnum | 0); - - JSC::throwException(globalObject, throwScope, exception.get()); - return true; - } - return false; -} - -static JSC::EncodedJSValue runInContext(NodeVMGlobalObject* globalObject, NodeVMScript* script, JSObject* contextifiedObject, JSValue optionsArg, bool allowStringInPlaceOfOptions = false) -{ - - auto& vm = JSC::getVM(globalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - - RunningScriptOptions options; - if (allowStringInPlaceOfOptions && optionsArg.isString()) { - options.filename = optionsArg.toWTFString(globalObject); - RETURN_IF_EXCEPTION(throwScope, {}); - } else if (!options.fromJS(globalObject, vm, throwScope, optionsArg)) { - RETURN_IF_EXCEPTION(throwScope, {}); - options = {}; - } - - // Set the contextified object before evaluating - globalObject->setContextifiedObject(contextifiedObject); - - NakedPtr exception; - JSValue result = JSC::evaluate(globalObject, script->source(), globalObject, exception); - - if (exception) [[unlikely]] { - if (handleException(globalObject, vm, exception, throwScope)) { - return {}; - } - JSC::throwException(globalObject, throwScope, exception.get()); - return {}; - } - - return JSValue::encode(result); -} - -JSC_DEFINE_HOST_FUNCTION(scriptConstructorCall, (JSGlobalObject * globalObject, CallFrame* callFrame)) -{ - return constructScript(globalObject, callFrame); -} - -JSC_DEFINE_HOST_FUNCTION(scriptConstructorConstruct, (JSGlobalObject * globalObject, CallFrame* callFrame)) -{ - return constructScript(globalObject, callFrame, callFrame->newTarget()); -} - -JSC_DEFINE_HOST_FUNCTION(scriptCreateCachedData, (JSGlobalObject * globalObject, CallFrame* callFrame)) -{ - auto& vm = JSC::getVM(globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); - - JSValue thisValue = callFrame->thisValue(); - auto* script = jsDynamicCast(thisValue); - if (!script) [[unlikely]] { - return ERR::INVALID_ARG_VALUE(scope, globalObject, "this"_s, thisValue, "must be a Script"_s); - } - - const JSC::SourceCode& source = script->source(); - return createCachedData(globalObject, source); -} - -JSC_DEFINE_HOST_FUNCTION(scriptRunInContext, (JSGlobalObject * globalObject, CallFrame* callFrame)) -{ - auto& vm = JSC::getVM(globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); - - JSValue thisValue = callFrame->thisValue(); - auto* script = jsDynamicCast(thisValue); - if (!script) [[unlikely]] { - return ERR::INVALID_ARG_VALUE(scope, globalObject, "this"_s, thisValue, "must be a Script"_s); - } - - ArgList args(callFrame); - JSValue contextArg = args.at(0); - if (contextArg.isUndefinedOrNull()) { - return ERR::INVALID_ARG_TYPE(scope, globalObject, "context"_s, "object"_s, contextArg); - } - - if (!contextArg.isObject()) { - return ERR::INVALID_ARG_TYPE(scope, globalObject, "context"_s, "object"_s, contextArg); - } - - JSObject* context = asObject(contextArg); - auto* zigGlobalObject = defaultGlobalObject(globalObject); - JSValue scopeValue = zigGlobalObject->vmModuleContextMap()->get(context); - if (scopeValue.isUndefined()) { - return INVALID_ARG_VALUE_VM_VARIATION(scope, globalObject, "contextifiedObject"_s, context); - } - - NodeVMGlobalObject* nodeVmGlobalObject = jsDynamicCast(scopeValue); - if (!nodeVmGlobalObject) { - return INVALID_ARG_VALUE_VM_VARIATION(scope, globalObject, "contextifiedObject"_s, context); - } - - return runInContext(nodeVmGlobalObject, script, context, args.at(1)); -} - -JSC_DEFINE_HOST_FUNCTION(scriptRunInThisContext, (JSGlobalObject * globalObject, CallFrame* callFrame)) -{ - auto& vm = JSC::getVM(globalObject); - JSValue thisValue = callFrame->thisValue(); - auto* script = jsDynamicCast(thisValue); - auto throwScope = DECLARE_THROW_SCOPE(vm); - - if (!script) [[unlikely]] { - return ERR::INVALID_ARG_VALUE(throwScope, globalObject, "this"_s, thisValue, "must be a Script"_s); - } - - JSValue contextArg = callFrame->argument(0); - if (contextArg.isUndefined()) { - contextArg = JSC::constructEmptyObject(globalObject); - } - - if (!contextArg.isObject()) { - return ERR::INVALID_ARG_TYPE(throwScope, globalObject, "context"_s, "object"_s, contextArg); - } - - RunningScriptOptions options; - if (!options.fromJS(globalObject, vm, throwScope, contextArg)) { - RETURN_IF_EXCEPTION(throwScope, {}); - options = {}; - } - - NakedPtr exception; - JSValue result = JSC::evaluate(globalObject, script->source(), globalObject, exception); - - if (exception) [[unlikely]] { - if (handleException(globalObject, vm, exception, throwScope)) { - return {}; - } - JSC::throwException(globalObject, throwScope, exception.get()); - return {}; - } - - RETURN_IF_EXCEPTION(throwScope, {}); - return JSValue::encode(result); -} - -JSC_DEFINE_CUSTOM_GETTER(scriptGetSourceMapURL, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValueEncoded, PropertyName)) -{ - auto& vm = JSC::getVM(globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); - JSValue thisValue = JSValue::decode(thisValueEncoded); - auto* script = jsDynamicCast(thisValue); - if (!script) [[unlikely]] { - return ERR::INVALID_ARG_VALUE(scope, globalObject, "this"_s, thisValue, "must be a Script"_s); - } - - const auto& url = script->source().provider()->sourceMappingURLDirective(); - return JSValue::encode(jsString(vm, url)); -} - -JSC_DEFINE_CUSTOM_GETTER(scriptGetCachedData, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValueEncoded, PropertyName)) -{ - auto& vm = JSC::getVM(globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); - JSValue thisValue = JSValue::decode(thisValueEncoded); - auto* script = jsDynamicCast(thisValue); - if (!script) [[unlikely]] { - return ERR::INVALID_ARG_VALUE(scope, globalObject, "this"_s, thisValue, "must be a Script"_s); - } - - if (auto* buffer = script->getBytecodeBuffer()) { - return JSValue::encode(buffer); - } - - return JSValue::encode(jsUndefined()); -} - -JSC_DEFINE_CUSTOM_GETTER(scriptGetCachedDataProduced, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValueEncoded, PropertyName)) -{ - auto& vm = JSC::getVM(globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); - JSValue thisValue = JSValue::decode(thisValueEncoded); - auto* script = jsDynamicCast(thisValue); - if (!script) [[unlikely]] { - return ERR::INVALID_ARG_VALUE(scope, globalObject, "this"_s, thisValue, "must be a Script"_s); - } - - return JSValue::encode(jsBoolean(script->cachedDataProduced())); -} - -JSC_DEFINE_CUSTOM_GETTER(scriptGetCachedDataRejected, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValueEncoded, PropertyName)) -{ - auto& vm = JSC::getVM(globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); - JSValue thisValue = JSValue::decode(thisValueEncoded); - auto* script = jsDynamicCast(thisValue); - if (!script) [[unlikely]] { - return ERR::INVALID_ARG_VALUE(scope, globalObject, "this"_s, thisValue, "must be a Script"_s); - } - - switch (script->cachedDataRejected()) { - case TriState::True: - return JSValue::encode(jsBoolean(true)); - case TriState::False: - return JSValue::encode(jsBoolean(false)); - default: - return JSValue::encode(jsUndefined()); - } -} - JSC_DEFINE_HOST_FUNCTION(vmModuleRunInNewContext, (JSGlobalObject * globalObject, CallFrame* callFrame)) { VM& vm = globalObject->vm(); @@ -1064,21 +726,29 @@ JSC_DEFINE_HOST_FUNCTION(vmModuleRunInNewContext, (JSGlobalObject * globalObject JSObject* sandbox = asObject(contextArg); + JSValue contextOptionsArg = callFrame->argument(2); + + NodeVMContextOptions contextOptions {}; + + if (auto encodedException = getNodeVMContextOptions(globalObject, vm, scope, contextOptionsArg, contextOptions, "contextCodeGeneration")) { + return *encodedException; + } + // Create context and run code auto* context = NodeVMGlobalObject::create(vm, - defaultGlobalObject(globalObject)->NodeVMGlobalObjectStructure()); + defaultGlobalObject(globalObject)->NodeVMGlobalObjectStructure(), + contextOptions); context->setContextifiedObject(sandbox); JSValue optionsArg = callFrame->argument(2); - ScriptOptions options; + ScriptOptions options(optionsArg.toWTFString(globalObject), OrdinalNumber::fromZeroBasedInt(0), OrdinalNumber::fromZeroBasedInt(0)); if (optionsArg.isString()) { options.filename = optionsArg.toWTFString(globalObject); RETURN_IF_EXCEPTION(scope, {}); } else if (!options.fromJS(globalObject, vm, scope, optionsArg)) { RETURN_IF_EXCEPTION(scope, {}); - options = {}; } auto sourceCode = SourceCode( @@ -1091,7 +761,7 @@ JSC_DEFINE_HOST_FUNCTION(vmModuleRunInNewContext, (JSGlobalObject * globalObject options.lineOffset.zeroBasedInt(), options.columnOffset.zeroBasedInt()); - NakedPtr exception; + NakedPtr exception; JSValue result = JSC::evaluate(context, sourceCode, context, exception); if (exception) [[unlikely]] { @@ -1107,7 +777,7 @@ JSC_DEFINE_HOST_FUNCTION(vmModuleRunInNewContext, (JSGlobalObject * globalObject JSC_DEFINE_HOST_FUNCTION(vmModuleRunInThisContext, (JSGlobalObject * globalObject, CallFrame* callFrame)) { - auto& vm = JSC::getVM(globalObject); + VM& vm = JSC::getVM(globalObject); auto sourceStringValue = callFrame->argument(0); auto throwScope = DECLARE_THROW_SCOPE(vm); @@ -1119,20 +789,19 @@ JSC_DEFINE_HOST_FUNCTION(vmModuleRunInThisContext, (JSGlobalObject * globalObjec RETURN_IF_EXCEPTION(throwScope, encodedJSUndefined()); JSValue optionsArg = callFrame->argument(1); - ScriptOptions options; + ScriptOptions options(optionsArg.toWTFString(globalObject), OrdinalNumber::fromZeroBasedInt(0), OrdinalNumber::fromZeroBasedInt(0)); if (optionsArg.isString()) { options.filename = optionsArg.toWTFString(globalObject); RETURN_IF_EXCEPTION(throwScope, {}); } else if (!options.fromJS(globalObject, vm, throwScope, optionsArg)) { RETURN_IF_EXCEPTION(throwScope, encodedJSUndefined()); - options = {}; } SourceCode source( JSC::StringSourceProvider::create(sourceString, JSC::SourceOrigin(WTF::URL::fileURLWithFileSystemPath(options.filename)), options.filename, JSC::SourceTaintedOrigin::Untainted, TextPosition(options.lineOffset, options.columnOffset)), options.lineOffset.zeroBasedInt(), options.columnOffset.zeroBasedInt()); - WTF::NakedPtr exception; + WTF::NakedPtr exception; JSValue result = JSC::evaluate(globalObject, source, globalObject, exception); if (exception) [[unlikely]] { @@ -1203,7 +872,7 @@ JSC_DEFINE_HOST_FUNCTION(vmModuleCompileFunction, (JSGlobalObject * globalObject SourceOrigin sourceOrigin = JSC::SourceOrigin(WTF::URL::fileURLWithFileSystemPath(options.filename)); // Process contextExtensions if they exist - JSScope* functionScope = !!options.parsingContext ? options.parsingContext : globalObject; + JSScope* functionScope = options.parsingContext ? options.parsingContext : globalObject; if (!options.contextExtensions.isUndefinedOrNull() && !options.contextExtensions.isEmpty() && options.contextExtensions.isObject() && isArray(globalObject, options.contextExtensions)) { auto* contextExtensionsArray = jsCast(options.contextExtensions); @@ -1230,51 +899,17 @@ JSC_DEFINE_HOST_FUNCTION(vmModuleCompileFunction, (JSGlobalObject * globalObject options.parsingContext->setGlobalScopeExtension(functionScope); // Create the function using constructAnonymousFunction with the appropriate scope chain - JSFunction* function = constructAnonymousFunction(globalObject, ArgList(constructFunctionArgs), sourceOrigin, options.filename, JSC::SourceTaintedOrigin::Untainted, TextPosition(options.lineOffset, options.columnOffset), functionScope); + JSFunction* function = constructAnonymousFunction(globalObject, ArgList(constructFunctionArgs), sourceOrigin, WTFMove(options), JSC::SourceTaintedOrigin::Untainted, functionScope); RETURN_IF_EXCEPTION(scope, {}); - if (!function) + if (!function) { return throwVMError(globalObject, scope, "Failed to compile function"_s); + } return JSValue::encode(function); } -JSC_DEFINE_HOST_FUNCTION(scriptRunInNewContext, (JSGlobalObject * globalObject, CallFrame* callFrame)) -{ - auto& vm = JSC::getVM(globalObject); - NodeVMScript* script = jsDynamicCast(callFrame->thisValue()); - JSValue contextObjectValue = callFrame->argument(0); - // TODO: options - // JSValue optionsObjectValue = callFrame->argument(1); - auto scope = DECLARE_THROW_SCOPE(vm); - - if (!script) { - throwTypeError(globalObject, scope, "this.runInContext is not a function"_s); - return {}; - } - - if (!contextObjectValue || contextObjectValue.isUndefinedOrNull()) { - contextObjectValue = JSC::constructEmptyObject(globalObject); - } - - if (!contextObjectValue || !contextObjectValue.isObject()) [[unlikely]] { - throwTypeError(globalObject, scope, "Context must be an object"_s); - return {}; - } - - // we don't care about options for now - // TODO: options - // bool didThrow = false; - - auto* zigGlobal = defaultGlobalObject(globalObject); - JSObject* context = asObject(contextObjectValue); - auto* targetContext = NodeVMGlobalObject::create( - vm, zigGlobal->NodeVMGlobalObjectStructure()); - - return runInContext(targetContext, script, context, callFrame->argument(1)); -} - Structure* createNodeVMGlobalObjectStructure(JSC::VM& vm) { return NodeVMGlobalObject::createStructure(vm, jsNull()); @@ -1283,7 +918,8 @@ Structure* createNodeVMGlobalObjectStructure(JSC::VM& vm) NodeVMGlobalObject* createContextImpl(JSC::VM& vm, JSGlobalObject* globalObject, JSObject* sandbox) { auto* targetContext = NodeVMGlobalObject::create(vm, - defaultGlobalObject(globalObject)->NodeVMGlobalObjectStructure()); + defaultGlobalObject(globalObject)->NodeVMGlobalObjectStructure(), + NodeVMContextOptions {}); // Set sandbox as contextified object targetContext->setContextifiedObject(sandbox); @@ -1301,7 +937,7 @@ JSC_DEFINE_HOST_FUNCTION(vmModule_createContext, (JSGlobalObject * globalObject, auto scope = DECLARE_THROW_SCOPE(vm); JSValue contextArg = callFrame->argument(0); - if (contextArg.isUndefinedOrNull()) { + if (contextArg.isUndefined()) { contextArg = JSC::constructEmptyObject(globalObject); } @@ -1316,31 +952,17 @@ JSC_DEFINE_HOST_FUNCTION(vmModule_createContext, (JSGlobalObject * globalObject, return ERR::INVALID_ARG_TYPE(scope, globalObject, "options"_s, "object"_s, optionsArg); } - // If options is provided, validate name and origin properties - if (optionsArg.isObject()) { - JSObject* options = asObject(optionsArg); + NodeVMContextOptions contextOptions {}; - // Check name property - if (JSValue nameValue = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "name"_s))) { - RETURN_IF_EXCEPTION(scope, {}); - if (!nameValue.isUndefined() && !nameValue.isString()) { - return ERR::INVALID_ARG_TYPE(scope, globalObject, "options.name"_s, "string"_s, nameValue); - } - } - - // Check origin property - if (JSValue originValue = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "origin"_s))) { - RETURN_IF_EXCEPTION(scope, {}); - if (!originValue.isUndefined() && !originValue.isString()) { - return ERR::INVALID_ARG_TYPE(scope, globalObject, "options.origin"_s, "string"_s, originValue); - } - } + if (auto encodedException = getNodeVMContextOptions(globalObject, vm, scope, optionsArg, contextOptions, "codeGeneration")) { + return *encodedException; } JSObject* sandbox = asObject(contextArg); auto* targetContext = NodeVMGlobalObject::create(vm, - defaultGlobalObject(globalObject)->NodeVMGlobalObjectStructure()); + defaultGlobalObject(globalObject)->NodeVMGlobalObjectStructure(), + contextOptions); // Set sandbox as contextified object targetContext->setContextifiedObject(sandbox); @@ -1369,50 +991,6 @@ JSC_DEFINE_HOST_FUNCTION(vmModule_isContext, (JSGlobalObject * globalObject, Cal return JSValue::encode(jsBoolean(isContext)); } -class NodeVMScriptPrototype final : public JSNonFinalObject { -public: - using Base = JSNonFinalObject; - - static NodeVMScriptPrototype* create(VM& vm, JSGlobalObject* globalObject, Structure* structure) - { - NodeVMScriptPrototype* ptr = new (NotNull, allocateCell(vm)) NodeVMScriptPrototype(vm, structure); - ptr->finishCreation(vm); - return ptr; - } - - DECLARE_INFO; - template - static GCClient::IsoSubspace* subspaceFor(VM& vm) - { - STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(NodeVMScriptPrototype, Base); - return &vm.plainObjectSpace(); - } - static Structure* createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) - { - return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); - } - -private: - NodeVMScriptPrototype(VM& vm, Structure* structure) - : Base(vm, structure) - { - } - - void finishCreation(VM&); -}; -STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(NodeVMScriptPrototype, NodeVMScriptPrototype::Base); - -static const struct HashTableValue scriptPrototypeTableValues[] = { - { "createCachedData"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, scriptCreateCachedData, 1 } }, - { "runInContext"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, scriptRunInContext, 2 } }, - { "runInNewContext"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, scriptRunInNewContext, 2 } }, - { "runInThisContext"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, scriptRunInThisContext, 2 } }, - { "sourceMapURL"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, scriptGetSourceMapURL, nullptr } }, - { "cachedData"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, scriptGetCachedData, nullptr } }, - { "cachedDataProduced"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, scriptGetCachedDataProduced, nullptr } }, - { "cachedDataRejected"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, scriptGetCachedDataRejected, nullptr } }, -}; - // NodeVMGlobalObject* NodeVMGlobalObject::create(JSC::VM& vm, JSC::Structure* structure) // { // auto* obj = new (NotNull, allocateCell(vm)) NodeVMGlobalObject(vm, structure); @@ -1435,166 +1013,16 @@ static const struct HashTableValue scriptPrototypeTableValues[] = { // // auto* thisObject = jsCast(cell); // // visitor.append(thisObject->m_proxyTarget); // } - -const ClassInfo NodeVMScriptPrototype::s_info = { "Script"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NodeVMScriptPrototype) }; -const ClassInfo NodeVMScript::s_info = { "Script"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NodeVMScript) }; -const ClassInfo NodeVMScriptConstructor::s_info = { "Script"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NodeVMScriptConstructor) }; const ClassInfo NodeVMGlobalObject::s_info = { "NodeVMGlobalObject"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NodeVMGlobalObject) }; -JSC::ProgramExecutable* NodeVMScript::createExecutable() -{ - auto& vm = JSC::getVM(globalObject()); - m_cachedExecutable.set(vm, this, JSC::ProgramExecutable::create(globalObject(), m_source)); - return m_cachedExecutable.get(); -} - -void NodeVMScript::cacheBytecode() -{ - if (!m_cachedExecutable) { - createExecutable(); - } - - m_cachedBytecode = getBytecode(globalObject(), m_cachedExecutable.get(), m_source); - m_cachedDataProduced = m_cachedBytecode != nullptr; -} - -JSC::JSUint8Array* NodeVMScript::getBytecodeBuffer() -{ - if (!m_options.produceCachedData) { - return nullptr; - } - - if (!m_cachedBytecodeBuffer) { - if (!m_cachedBytecode) { - cacheBytecode(); - } - - ASSERT(m_cachedBytecode); - - std::span bytes = m_cachedBytecode->span(); - m_cachedBytecodeBuffer.set(vm(), this, WebCore::createBuffer(globalObject(), bytes)); - } - - ASSERT(m_cachedBytecodeBuffer); - return m_cachedBytecodeBuffer.get(); -} - -DEFINE_VISIT_CHILDREN(NodeVMScript); - -template -void NodeVMScript::visitChildrenImpl(JSCell* cell, Visitor& visitor) -{ - NodeVMScript* thisObject = jsCast(cell); - ASSERT_GC_OBJECT_INHERITS(thisObject, info()); - Base::visitChildren(thisObject, visitor); - visitor.append(thisObject->m_cachedExecutable); - visitor.append(thisObject->m_cachedBytecodeBuffer); -} - -NodeVMScriptConstructor::NodeVMScriptConstructor(VM& vm, Structure* structure) - : NodeVMScriptConstructor::Base(vm, structure, scriptConstructorCall, scriptConstructorConstruct) -{ -} - -NodeVMScriptConstructor* NodeVMScriptConstructor::create(VM& vm, JSGlobalObject* globalObject, Structure* structure, JSObject* prototype) -{ - NodeVMScriptConstructor* ptr = new (NotNull, allocateCell(vm)) NodeVMScriptConstructor(vm, structure); - ptr->finishCreation(vm, prototype); - return ptr; -} - -void NodeVMScriptConstructor::finishCreation(VM& vm, JSObject* prototype) -{ - Base::finishCreation(vm, 1, "Script"_s, PropertyAdditionMode::WithStructureTransition); - putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); - ASSERT(inherits(info())); -} - -void NodeVMScriptPrototype::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - reifyStaticProperties(vm, NodeVMScript::info(), scriptPrototypeTableValues, *this); - JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); -} - -JSObject* NodeVMScript::createPrototype(VM& vm, JSGlobalObject* globalObject) -{ - return NodeVMScriptPrototype::create(vm, globalObject, NodeVMScriptPrototype::createStructure(vm, globalObject, globalObject->objectPrototype())); -} - -NodeVMScript* NodeVMScript::create(VM& vm, JSGlobalObject* globalObject, Structure* structure, SourceCode source, ScriptOptions options) -{ - NodeVMScript* ptr = new (NotNull, allocateCell(vm)) NodeVMScript(vm, structure, source, WTFMove(options)); - ptr->finishCreation(vm); - return ptr; -} - -void NodeVMScript::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - ASSERT(inherits(info())); -} - -void NodeVMScript::destroy(JSCell* cell) -{ - static_cast(cell)->NodeVMScript::~NodeVMScript(); -} - -JSC::JSValue createNodeVMBinding(Zig::GlobalObject* globalObject) -{ - VM& vm = globalObject->vm(); - auto* obj = constructEmptyObject(globalObject); - obj->putDirect( - vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "Script"_s)), - defaultGlobalObject(globalObject)->NodeVMScript(), 0); - obj->putDirect( - vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "createContext"_s)), - JSC::JSFunction::create(vm, globalObject, 0, "createContext"_s, vmModule_createContext, ImplementationVisibility::Public), 0); - obj->putDirect( - vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "isContext"_s)), - JSC::JSFunction::create(vm, globalObject, 0, "isContext"_s, vmModule_isContext, ImplementationVisibility::Public), 0); - obj->putDirect( - vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "runInNewContext"_s)), - JSC::JSFunction::create(vm, globalObject, 0, "runInNewContext"_s, vmModuleRunInNewContext, ImplementationVisibility::Public), 0); - obj->putDirect( - vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "runInThisContext"_s)), - JSC::JSFunction::create(vm, globalObject, 0, "runInThisContext"_s, vmModuleRunInThisContext, ImplementationVisibility::Public), 0); - obj->putDirect( - vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "compileFunction"_s)), - JSC::JSFunction::create(vm, globalObject, 0, "compileFunction"_s, vmModuleCompileFunction, ImplementationVisibility::Public), 0); - return obj; -} - -void configureNodeVM(JSC::VM& vm, Zig::GlobalObject* globalObject) -{ - globalObject->m_NodeVMScriptClassStructure.initLater( - [](LazyClassStructure::Initializer& init) { - auto prototype = NodeVMScript::createPrototype(init.vm, init.global); - auto* structure = NodeVMScript::createStructure(init.vm, init.global, prototype); - auto* constructorStructure = NodeVMScriptConstructor::createStructure( - init.vm, init.global, init.global->m_functionPrototype.get()); - auto* constructor = NodeVMScriptConstructor::create( - init.vm, init.global, constructorStructure, prototype); - init.setPrototype(prototype); - init.setStructure(structure); - init.setConstructor(constructor); - }); - - globalObject->m_cachedNodeVMGlobalObjectStructure.initLater( - [](const JSC::LazyProperty::Initializer& init) { - init.set(createNodeVMGlobalObjectStructure(init.vm)); - }); -} - bool NodeVMGlobalObject::deleteProperty(JSCell* cell, JSGlobalObject* globalObject, PropertyName propertyName, JSC::DeletePropertySlot& slot) { - auto* thisObject = jsCast(cell); if (!thisObject->m_sandbox) [[unlikely]] { return Base::deleteProperty(cell, globalObject, propertyName, slot); } - auto& vm = JSC::getVM(globalObject); + VM& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); auto* sandbox = thisObject->m_sandbox.get(); @@ -1621,193 +1049,319 @@ void NodeVMGlobalObject::getOwnPropertyNames(JSObject* cell, JSGlobalObject* glo Base::getOwnPropertyNames(cell, globalObject, propertyNames, mode); } -static JSC::JSFunction* constructAnonymousFunction(JSC::JSGlobalObject* globalObject, const ArgList& args, const SourceOrigin& sourceOrigin, const String& fileName, JSC::SourceTaintedOrigin sourceTaintOrigin, TextPosition position, JSC::JSScope* scope) +JSC_DEFINE_HOST_FUNCTION(vmIsModuleNamespaceObject, (JSGlobalObject * globalObject, CallFrame* callFrame)) { VM& vm = globalObject->vm(); - auto throwScope = DECLARE_THROW_SCOPE(vm); - - // wrap the arguments in an anonymous function expression - int startOffset = 0; - String code = stringifyAnonymousFunction(globalObject, args, throwScope, &startOffset); - EXCEPTION_ASSERT(!!throwScope.exception() == code.isNull()); - - position.m_column = OrdinalNumber::fromZeroBasedInt(position.m_column.zeroBasedInt()); - SourceCode sourceCode( - JSC::StringSourceProvider::create(code, sourceOrigin, fileName, sourceTaintOrigin, position, SourceProviderSourceType::Program), - position.m_line.oneBasedInt(), position.m_column.oneBasedInt()); - - LexicallyScopedFeatures lexicallyScopedFeatures = globalObject->globalScopeExtension() ? TaintedByWithScopeLexicallyScopedFeature : NoLexicallyScopedFeatures; - - ParserError error; - bool isEvalNode = false; - - // use default name - Identifier name; - std::unique_ptr program; - - if (code.is8Bit()) { - Parser> parser(vm, sourceCode, ImplementationVisibility::Public, JSParserBuiltinMode::NotBuiltin, - lexicallyScopedFeatures, JSParserScriptMode::Classic, SourceParseMode::ProgramMode, - FunctionMode::None, SuperBinding::NotNeeded, ConstructorKind::None, DerivedContextType::None, - isEvalNode, EvalContextType::None, nullptr); - - program = parser.parse(error, name, ParsingContext::Normal); - } else { - Parser> parser(vm, sourceCode, ImplementationVisibility::Public, JSParserBuiltinMode::NotBuiltin, - lexicallyScopedFeatures, JSParserScriptMode::Classic, SourceParseMode::ProgramMode, - FunctionMode::None, SuperBinding::NotNeeded, ConstructorKind::None, DerivedContextType::None, - isEvalNode, EvalContextType::None, nullptr); - - program = parser.parse(error, name, ParsingContext::Normal); - } - - if (!program) { - RELEASE_ASSERT(error.isValid()); - auto exception = error.toErrorObject(globalObject, sourceCode, -1); - throwException(globalObject, throwScope, exception); - return nullptr; - } - - // the code we passed in should be a single expression statement containing a function expression - StatementNode* statement = program->singleStatement(); - if (!statement || !statement->isExprStatement()) { - JSToken token; - error = ParserError(ParserError::SyntaxError, ParserError::SyntaxErrorIrrecoverable, token, "Parser error"_s, -1); - auto exception = error.toErrorObject(globalObject, sourceCode, -1); - throwException(globalObject, throwScope, exception); - return nullptr; - } - - ExprStatementNode* exprStatement = static_cast(statement); - ExpressionNode* expression = exprStatement->expr(); - if (!expression || !expression->isFuncExprNode()) { - throwSyntaxError(globalObject, throwScope, "Expected a function expression"_s); - return nullptr; - } - - FunctionMetadataNode* metadata = static_cast(expression)->metadata(); - ASSERT(metadata); - if (!metadata) - return nullptr; - - // metadata->setStartOffset(startOffset); - - ConstructAbility constructAbility = constructAbilityForParseMode(metadata->parseMode()); - UnlinkedFunctionExecutable* unlinkedFunctionExecutable = UnlinkedFunctionExecutable::create( - vm, - sourceCode, - metadata, - UnlinkedNormalFunction, - constructAbility, - InlineAttribute::None, - JSParserScriptMode::Classic, - nullptr, - std::nullopt, - std::nullopt, - DerivedContextType::None, - NeedsClassFieldInitializer::No, - PrivateBrandRequirement::None); - - unlinkedFunctionExecutable->recordParse(program->features(), metadata->lexicallyScopedFeatures(), /* hasCapturedVariables */ false); - - FunctionExecutable* functionExecutable = unlinkedFunctionExecutable->link(vm, nullptr, sourceCode, std::nullopt); - - JSScope* functionScope = scope ? scope : globalObject->globalScope(); - - Structure* structure = JSFunction::selectStructureForNewFuncExp(globalObject, functionExecutable); - - JSFunction* function = JSFunction::create(vm, globalObject, functionExecutable, functionScope, structure); - return function; -} - -// Helper function to create an anonymous function expression with parameters -static String stringifyAnonymousFunction(JSGlobalObject* globalObject, const ArgList& args, ThrowScope& scope, int* outOffset) -{ - // How we stringify functions is important for creating anonymous function expressions - String program; - if (args.isEmpty()) { - // No arguments, just an empty function body - program = "(function () {\n\n})"_s; - // program = "(function () {})"_s; - } else if (args.size() == 1) { - // Just the function body - auto body = args.at(0).toWTFString(globalObject); - RETURN_IF_EXCEPTION(scope, {}); - - program = tryMakeString("(function () {"_s, body, "})"_s); - *outOffset = "(function () {"_s.length(); - - if (!program) [[unlikely]] { - throwOutOfMemoryError(globalObject, scope); - return {}; - } - } else { - // Process parameters and body - unsigned parameterCount = args.size() - 1; - StringBuilder paramString; - - for (unsigned i = 0; i < parameterCount; ++i) { - auto param = args.at(i).toWTFString(globalObject); - RETURN_IF_EXCEPTION(scope, {}); - - if (i > 0) - paramString.append(", "_s); - - paramString.append(param); - } - - auto body = args.at(parameterCount).toWTFString(globalObject); - RETURN_IF_EXCEPTION(scope, {}); - - program = tryMakeString("(function ("_s, paramString.toString(), ") {"_s, body, "})"_s); - *outOffset = "(function ("_s.length() + paramString.length() + ") {"_s.length(); - - if (!program) [[unlikely]] { - throwOutOfMemoryError(globalObject, scope); - return {}; - } - } - - return program; -} - -static RefPtr getBytecode(JSGlobalObject* globalObject, JSC::ProgramExecutable* executable, JSC::SourceCode source) -{ - auto& vm = JSC::getVM(globalObject); - JSC::CodeCache* cache = vm.codeCache(); - JSC::ParserError parserError; - JSC::UnlinkedProgramCodeBlock* unlinked = cache->getUnlinkedProgramCodeBlock(vm, executable, source, {}, parserError); - if (!unlinked || parserError.isValid()) { - return nullptr; - } - JSC::LexicallyScopedFeatures lexicallyScopedFeatures = globalObject->globalScopeExtension() ? TaintedByWithScopeLexicallyScopedFeature : NoLexicallyScopedFeatures; - JSC::BytecodeCacheError bytecodeCacheError; - FileSystem::FileHandle fileHandle; - return JSC::serializeBytecode(vm, unlinked, source, JSC::SourceCodeType::ProgramType, lexicallyScopedFeatures, JSParserScriptMode::Classic, fileHandle, bytecodeCacheError, {}); -} - -static JSC::EncodedJSValue createCachedData(JSGlobalObject* globalObject, JSC::SourceCode source) -{ - auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); - JSC::ProgramExecutable* executable = JSC::ProgramExecutable::create(globalObject, source); - RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(JSC::jsBoolean(false)); // TODO(@heimskr): implement - RefPtr bytecode = getBytecode(globalObject, executable, source); - RETURN_IF_EXCEPTION(scope, {}); + // JSValue argument = callFrame->argument(0); + // if (!argument.isObject()) { + // return JSValue::encode(JSC::jsBoolean(false)); + // } - if (!bytecode) [[unlikely]] { - return throwVMError(globalObject, scope, "createCachedData failed"_s); + // JSObject* object = asObject(argument); +} + +JSC::JSValue createNodeVMBinding(Zig::GlobalObject* globalObject) +{ + VM& vm = globalObject->vm(); + auto* obj = constructEmptyObject(globalObject); + obj->putDirect( + vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "Script"_s)), + defaultGlobalObject(globalObject)->NodeVMScript(), 0); + obj->putDirect( + vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "Module"_s)), + defaultGlobalObject(globalObject)->NodeVMSourceTextModule(), 0); + obj->putDirect( + vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "createContext"_s)), + JSC::JSFunction::create(vm, globalObject, 0, "createContext"_s, vmModule_createContext, ImplementationVisibility::Public), 0); + obj->putDirect( + vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "isContext"_s)), + JSC::JSFunction::create(vm, globalObject, 0, "isContext"_s, vmModule_isContext, ImplementationVisibility::Public), 0); + obj->putDirect( + vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "runInNewContext"_s)), + JSC::JSFunction::create(vm, globalObject, 0, "runInNewContext"_s, vmModuleRunInNewContext, ImplementationVisibility::Public), 0); + obj->putDirect( + vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "runInThisContext"_s)), + JSC::JSFunction::create(vm, globalObject, 0, "runInThisContext"_s, vmModuleRunInThisContext, ImplementationVisibility::Public), 0); + obj->putDirect( + vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "compileFunction"_s)), + JSC::JSFunction::create(vm, globalObject, 0, "compileFunction"_s, vmModuleCompileFunction, ImplementationVisibility::Public), 0); + obj->putDirect( + vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "isModuleNamespaceObject"_s)), + JSC::JSFunction::create(vm, globalObject, 0, "isModuleNamespaceObject"_s, vmIsModuleNamespaceObject, ImplementationVisibility::Public), 1); + obj->putDirect( + vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "kUnlinked"_s)), + JSC::jsNumber(static_cast(NodeVMSourceTextModule::Status::Unlinked)), 0); + obj->putDirect( + vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "kLinking"_s)), + JSC::jsNumber(static_cast(NodeVMSourceTextModule::Status::Linking)), 0); + obj->putDirect( + vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "kLinked"_s)), + JSC::jsNumber(static_cast(NodeVMSourceTextModule::Status::Linked)), 0); + obj->putDirect( + vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "kEvaluating"_s)), + JSC::jsNumber(static_cast(NodeVMSourceTextModule::Status::Evaluating)), 0); + obj->putDirect( + vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "kEvaluated"_s)), + JSC::jsNumber(static_cast(NodeVMSourceTextModule::Status::Evaluated)), 0); + obj->putDirect( + vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "kErrored"_s)), + JSC::jsNumber(static_cast(NodeVMSourceTextModule::Status::Errored)), 0); + obj->putDirect( + vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "kSourceText"_s)), + JSC::jsNumber(static_cast(NodeVMModule::Type::SourceText)), 0); + obj->putDirect( + vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "kSynthetic"_s)), + JSC::jsNumber(static_cast(NodeVMModule::Type::Synthetic)), 0); + return obj; +} + +void configureNodeVM(JSC::VM& vm, Zig::GlobalObject* globalObject) +{ + globalObject->m_NodeVMScriptClassStructure.initLater( + [](LazyClassStructure::Initializer& init) { + auto prototype = NodeVMScript::createPrototype(init.vm, init.global); + auto* structure = NodeVMScript::createStructure(init.vm, init.global, prototype); + auto* constructorStructure = NodeVMScriptConstructor::createStructure( + init.vm, init.global, init.global->m_functionPrototype.get()); + auto* constructor = NodeVMScriptConstructor::create( + init.vm, init.global, constructorStructure, prototype); + init.setPrototype(prototype); + init.setStructure(structure); + init.setConstructor(constructor); + }); + + globalObject->m_NodeVMSourceTextModuleClassStructure.initLater( + [](LazyClassStructure::Initializer& init) { + auto prototype = NodeVMSourceTextModule::createPrototype(init.vm, init.global); + auto* structure = NodeVMSourceTextModule::createStructure(init.vm, init.global, prototype); + auto* constructorStructure = NodeVMModuleConstructor::createStructure( + init.vm, init.global, init.global->m_functionPrototype.get()); + auto* constructor = NodeVMModuleConstructor::create( + init.vm, init.global, constructorStructure, prototype); + init.setPrototype(prototype); + init.setStructure(structure); + init.setConstructor(constructor); + }); + + // globalObject->m_NodeVMSyntheticModuleClassStructure.initLater( + // [](LazyClassStructure::Initializer& init) { + // auto prototype = NodeVMSyntheticModule::createPrototype(init.vm, init.global); + // auto* structure = NodeVMSyntheticModule::createStructure(init.vm, init.global, prototype); + // auto* constructorStructure = NodeVMModuleConstructor::createStructure( + // init.vm, init.global, init.global->m_functionPrototype.get()); + // auto* constructor = NodeVMModuleConstructor::create( + // init.vm, init.global, constructorStructure, prototype); + // }); + + globalObject->m_cachedNodeVMGlobalObjectStructure.initLater( + [](const JSC::LazyProperty::Initializer& init) { + init.set(createNodeVMGlobalObjectStructure(init.vm)); + }); +} + +BaseVMOptions::BaseVMOptions(String filename) + : filename(WTFMove(filename)) +{ +} + +BaseVMOptions::BaseVMOptions(String filename, OrdinalNumber lineOffset, OrdinalNumber columnOffset) + : filename(WTFMove(filename)) + , lineOffset(lineOffset) + , columnOffset(columnOffset) +{ +} + +bool BaseVMOptions::fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg) +{ + JSObject* options = nullptr; + bool any = false; + + if (!optionsArg.isUndefined()) { + if (optionsArg.isObject()) { + options = asObject(optionsArg); + } else { + auto _ = ERR::INVALID_ARG_TYPE(scope, globalObject, "options"_s, "object"_s, optionsArg); + return false; + } + + if (JSValue filenameOpt = options->getIfPropertyExists(globalObject, builtinNames(vm).filenamePublicName())) { + if (filenameOpt.isString()) { + this->filename = filenameOpt.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, false); + any = true; + } else if (!filenameOpt.isUndefined()) { + ERR::INVALID_ARG_TYPE(scope, globalObject, "options.filename"_s, "string"_s, filenameOpt); + return false; + } + } else { + this->filename = "evalmachine."_s; + } + + if (JSValue lineOffsetOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "lineOffset"_s))) { + if (lineOffsetOpt.isAnyInt()) { + if (!lineOffsetOpt.isInt32()) { + ERR::OUT_OF_RANGE(scope, globalObject, "options.lineOffset"_s, std::numeric_limits().min(), std::numeric_limits().max(), lineOffsetOpt); + return false; + } + this->lineOffset = OrdinalNumber::fromZeroBasedInt(lineOffsetOpt.asInt32()); + any = true; + } else if (lineOffsetOpt.isNumber()) { + ERR::OUT_OF_RANGE(scope, globalObject, "options.lineOffset"_s, "an integer"_s, lineOffsetOpt); + return false; + } else if (!lineOffsetOpt.isUndefined()) { + ERR::INVALID_ARG_TYPE(scope, globalObject, "options.lineOffset"_s, "number"_s, lineOffsetOpt); + return false; + } + } + + if (JSValue columnOffsetOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "columnOffset"_s))) { + if (columnOffsetOpt.isAnyInt()) { + if (!columnOffsetOpt.isInt32()) { + ERR::OUT_OF_RANGE(scope, globalObject, "options.columnOffset"_s, std::numeric_limits().min(), std::numeric_limits().max(), columnOffsetOpt); + return false; + } + int columnOffsetValue = columnOffsetOpt.asInt32(); + + this->columnOffset = OrdinalNumber::fromZeroBasedInt(columnOffsetValue); + any = true; + } else if (columnOffsetOpt.isNumber()) { + ERR::OUT_OF_RANGE(scope, globalObject, "options.columnOffset"_s, "an integer"_s, columnOffsetOpt); + return false; + } else if (!columnOffsetOpt.isUndefined()) { + ERR::INVALID_ARG_TYPE(scope, globalObject, "options.columnOffset"_s, "number"_s, columnOffsetOpt); + return false; + } + } } - std::span bytes = bytecode->span(); - auto* buffer = WebCore::createBuffer(globalObject, bytes); + return any; +} +bool BaseVMOptions::validateProduceCachedData(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSObject* options, bool& outProduceCachedData) +{ + JSValue produceCachedDataOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "produceCachedData"_s)); + if (produceCachedDataOpt && !produceCachedDataOpt.isUndefined()) { + RETURN_IF_EXCEPTION(scope, {}); + if (!produceCachedDataOpt.isBoolean()) { + ERR::INVALID_ARG_TYPE(scope, globalObject, "options.produceCachedData"_s, "boolean"_s, produceCachedDataOpt); + return false; + } + outProduceCachedData = produceCachedDataOpt.asBoolean(); + return true; + } + return false; +} + +bool BaseVMOptions::validateCachedData(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSObject* options, WTF::Vector& outCachedData) +{ + JSValue cachedDataOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "cachedData"_s)); RETURN_IF_EXCEPTION(scope, {}); - ASSERT(buffer); - return JSValue::encode(buffer); + if (cachedDataOpt && !cachedDataOpt.isUndefined()) { + // Verify it's a Buffer, TypedArray or DataView and extract the data if it is. + if (extractCachedData(cachedDataOpt, outCachedData)) { + return true; + } + + ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.cachedData"_s, "Buffer, TypedArray, or DataView"_s, cachedDataOpt); + } + + return false; +} + +bool BaseVMOptions::validateTimeout(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSObject* options, std::optional& outTimeout) +{ + JSValue timeoutOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "timeout"_s)); + if (timeoutOpt && !timeoutOpt.isUndefined()) { + if (!timeoutOpt.isNumber()) { + ERR::INVALID_ARG_TYPE(scope, globalObject, "options.timeout"_s, "number"_s, timeoutOpt); + return false; + } + + ssize_t timeoutValue; + V::validateInteger(scope, globalObject, timeoutOpt, "options.timeout"_s, jsNumber(1), jsNumber(std::numeric_limits().max()), &timeoutValue); + RETURN_IF_EXCEPTION(scope, {}); + + outTimeout = timeoutValue; + return true; + } + return false; +} + +bool CompileFunctionOptions::fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg) +{ + this->parsingContext = globalObject; + bool any = BaseVMOptions::fromJS(globalObject, vm, scope, optionsArg); + RETURN_IF_EXCEPTION(scope, false); + + if (!optionsArg.isUndefined() && !optionsArg.isString()) { + JSObject* options = asObject(optionsArg); + + if (validateProduceCachedData(globalObject, vm, scope, options, this->produceCachedData)) { + RETURN_IF_EXCEPTION(scope, false); + any = true; + } + + if (validateCachedData(globalObject, vm, scope, options, this->cachedData)) { + RETURN_IF_EXCEPTION(scope, false); + any = true; + } + + JSValue parsingContextValue = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "parsingContext"_s)); + RETURN_IF_EXCEPTION(scope, {}); + + if (!parsingContextValue.isEmpty() && !parsingContextValue.isUndefined()) { + if (parsingContextValue.isNull() || !parsingContextValue.isObject()) + return ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.parsingContext"_s, "Context"_s, parsingContextValue); + + JSObject* context = asObject(parsingContextValue); + auto* zigGlobalObject = defaultGlobalObject(globalObject); + JSValue scopeValue = zigGlobalObject->vmModuleContextMap()->get(context); + + if (scopeValue.isUndefined()) + return ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.parsingContext"_s, "Context"_s, parsingContextValue); + + parsingContext = jsDynamicCast(scopeValue); + if (!parsingContext) + return ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.parsingContext"_s, "Context"_s, parsingContextValue); + + any = true; + } + + // Handle contextExtensions option + JSValue contextExtensionsValue = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "contextExtensions"_s)); + RETURN_IF_EXCEPTION(scope, {}); + + if (!contextExtensionsValue.isEmpty() && !contextExtensionsValue.isUndefined()) { + if (contextExtensionsValue.isNull() || !contextExtensionsValue.isObject()) + return ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.contextExtensions"_s, "Array"_s, contextExtensionsValue); + + if (auto* contextExtensionsObject = asObject(contextExtensionsValue)) { + if (!isArray(globalObject, contextExtensionsObject)) + return ERR::INVALID_ARG_TYPE(scope, globalObject, "options.contextExtensions"_s, "Array"_s, contextExtensionsValue); + + // Validate that all items in the array are objects + auto* contextExtensionsArray = jsCast(contextExtensionsValue); + unsigned length = contextExtensionsArray->length(); + for (unsigned i = 0; i < length; i++) { + JSValue extension = contextExtensionsArray->getIndexQuickly(i); + if (!extension.isObject()) + return ERR::INVALID_ARG_TYPE(scope, globalObject, "options.contextExtensions[0]"_s, "object"_s, extension); + } + } else { + return ERR::INVALID_ARG_TYPE(scope, globalObject, "options.contextExtensions"_s, "Array"_s, contextExtensionsValue); + } + + this->contextExtensions = contextExtensionsValue; + any = true; + } + } + + return any; } } // namespace Bun diff --git a/src/bun.js/bindings/NodeVM.h b/src/bun.js/bindings/NodeVM.h index 5e0e692170..13a913eafe 100644 --- a/src/bun.js/bindings/NodeVM.h +++ b/src/bun.js/bindings/NodeVM.h @@ -14,6 +14,27 @@ namespace Bun { +class NodeVMGlobalObject; +class NodeVMContextOptions; +class CompileFunctionOptions; + +namespace NodeVM { + +RefPtr getBytecode(JSGlobalObject* globalObject, JSC::ProgramExecutable* executable, const JSC::SourceCode& source); +RefPtr getBytecode(JSGlobalObject* globalObject, JSC::ModuleProgramExecutable* executable, const JSC::SourceCode& source); +bool extractCachedData(JSValue cachedDataValue, WTF::Vector& outCachedData); +String stringifyAnonymousFunction(JSGlobalObject* globalObject, const ArgList& args, ThrowScope& scope, int* outOffset); +JSC::EncodedJSValue createCachedData(JSGlobalObject* globalObject, const JSC::SourceCode& source); +NodeVMGlobalObject* createContextImpl(JSC::VM& vm, JSGlobalObject* globalObject, JSObject* sandbox); +bool handleException(JSGlobalObject* globalObject, VM& vm, NakedPtr exception, ThrowScope& throwScope); +std::optional getNodeVMContextOptions(JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSValue optionsArg, NodeVMContextOptions& outOptions, ASCIILiteral codeGenerationKey); +NodeVMGlobalObject* getGlobalObjectFromContext(JSGlobalObject* globalObject, JSValue contextValue, bool canThrow); +JSC::EncodedJSValue INVALID_ARG_VALUE_VM_VARIATION(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral name, JSC::JSValue value); +// For vm.compileFunction we need to return an anonymous function expression. This code is adapted from/inspired by JSC::constructFunction, which is used for function declarations. +JSC::JSFunction* constructAnonymousFunction(JSC::JSGlobalObject* globalObject, const ArgList& args, const SourceOrigin& sourceOrigin, CompileFunctionOptions&& options, JSC::SourceTaintedOrigin sourceTaintOrigin, JSC::JSScope* scope); + +} // namespace NodeVM + // This class represents a sandboxed global object for vm contexts class NodeVMGlobalObject final : public Bun::GlobalScope { using Base = Bun::GlobalScope; @@ -23,16 +44,18 @@ public: static constexpr JSC::DestructionMode needsDestruction = NeedsDestruction; template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm); - static NodeVMGlobalObject* create(JSC::VM& vm, JSC::Structure* structure); + static NodeVMGlobalObject* create(JSC::VM& vm, JSC::Structure* structure, NodeVMContextOptions options); static Structure* createStructure(JSC::VM& vm, JSC::JSValue prototype); DECLARE_INFO; DECLARE_VISIT_CHILDREN; - void finishCreation(JSC::VM&); + void finishCreation(JSC::VM&, NodeVMContextOptions options); static void destroy(JSCell* cell); void setContextifiedObject(JSC::JSObject* contextifiedObject); + JSC::JSObject* contextifiedObject() const { return m_sandbox.get(); } void clearContextifiedObject(); + void sigintReceived(); // Override property access to delegate to contextified object static bool getOwnPropertySlot(JSObject*, JSGlobalObject*, JSC::PropertyName, JSC::PropertySlot&); @@ -60,4 +83,39 @@ JSC_DECLARE_HOST_FUNCTION(vmModule_isContext); JSC_DECLARE_HOST_FUNCTION(vmModuleRunInNewContext); JSC_DECLARE_HOST_FUNCTION(vmModuleRunInThisContext); +class BaseVMOptions { +public: + String filename; + OrdinalNumber lineOffset = OrdinalNumber::fromZeroBasedInt(0); + OrdinalNumber columnOffset = OrdinalNumber::fromZeroBasedInt(0); + bool failed = false; + + BaseVMOptions() = default; + BaseVMOptions(String filename); + BaseVMOptions(String filename, OrdinalNumber lineOffset, OrdinalNumber columnOffset); + + bool fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg); + bool validateProduceCachedData(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSObject* options, bool& outProduceCachedData); + bool validateCachedData(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSObject* options, WTF::Vector& outCachedData); + bool validateTimeout(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSObject* options, std::optional& outTimeout); +}; + +class CompileFunctionOptions : public BaseVMOptions { +public: + WTF::Vector cachedData; + JSGlobalObject* parsingContext = nullptr; + JSValue contextExtensions; + bool produceCachedData = false; + + using BaseVMOptions::BaseVMOptions; + + bool fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg); +}; + +class NodeVMContextOptions final { +public: + bool allowStrings = true; + bool allowWasm = true; +}; + } // namespace Bun diff --git a/src/bun.js/bindings/NodeVMModule.cpp b/src/bun.js/bindings/NodeVMModule.cpp new file mode 100644 index 0000000000..66ce7c94b2 --- /dev/null +++ b/src/bun.js/bindings/NodeVMModule.cpp @@ -0,0 +1,395 @@ +#include "NodeVMModule.h" +#include "NodeVMSourceTextModule.h" + +#include "ErrorCode.h" +#include "JSDOMExceptionHandling.h" + +namespace Bun { + +NodeVMModuleRequest::NodeVMModuleRequest(WTF::String specifier, WTF::HashMap importAttributes) + : m_specifier(WTFMove(specifier)) + , m_importAttributes(WTFMove(importAttributes)) +{ +} + +void NodeVMModuleRequest::addImportAttribute(WTF::String key, WTF::String value) +{ + m_importAttributes.set(WTFMove(key), WTFMove(value)); +} + +JSArray* NodeVMModuleRequest::toJS(JSGlobalObject* globalObject) const +{ + JSArray* array = JSC::constructEmptyArray(globalObject, nullptr, 2); + array->putDirectIndex(globalObject, 0, JSC::jsString(globalObject->vm(), m_specifier)); + + JSObject* attributes = JSC::constructEmptyObject(globalObject); + for (const auto& [key, value] : m_importAttributes) { + attributes->putDirect(globalObject->vm(), JSC::Identifier::fromString(globalObject->vm(), key), JSC::jsString(globalObject->vm(), value), + PropertyAttribute::ReadOnly | PropertyAttribute::DontDelete); + } + array->putDirectIndex(globalObject, 1, attributes); + + return array; +} + +NodeVMModule::NodeVMModule(JSC::VM& vm, JSC::Structure* structure, WTF::String identifier, JSValue context) + : Base(vm, structure) + , m_identifier(WTFMove(identifier)) +{ + if (context.isObject()) { + m_context.set(vm, this, asObject(context)); + } +} + +bool NodeVMModule::finishInstantiate(JSC::JSGlobalObject* globalObject, WTF::Deque& stack, unsigned* dfsIndex) +{ + if (auto* thisObject = jsDynamicCast(this)) { + return thisObject->finishInstantiate(globalObject, stack, dfsIndex); + // } else if (auto* thisObject = jsDynamicCast(this)) { + // return thisObject->finishInstantiate(globalObject); + } + + return true; +} + +JSValue NodeVMModule::createModuleRecord(JSC::JSGlobalObject* globalObject) +{ + if (auto* thisObject = jsDynamicCast(this)) { + return thisObject->createModuleRecord(globalObject); + } + + ASSERT_NOT_REACHED(); + return JSC::jsUndefined(); +} + +AbstractModuleRecord* NodeVMModule::moduleRecord(JSC::JSGlobalObject* globalObject) +{ + if (auto* thisObject = jsDynamicCast(this)) { + return thisObject->moduleRecord(globalObject); + } + + ASSERT_NOT_REACHED(); + return nullptr; +} + +NodeVMModule* NodeVMModule::create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, ArgList args) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue disambiguator = args.at(2); + + if (disambiguator.isString()) { + return NodeVMSourceTextModule::create(vm, globalObject, args); + } + + if (disambiguator.inherits(JSArray::info())) { + // return NodeVMSyntheticModule::create(vm, globalObject, args); + } + + throwArgumentTypeError(*globalObject, scope, 2, "sourceText or syntheticExportNames"_s, "Module"_s, "Module"_s, "string or array"_s); + return nullptr; +} + +JSModuleNamespaceObject* NodeVMModule::namespaceObject(JSC::JSGlobalObject* globalObject) +{ + JSModuleNamespaceObject* object = m_namespaceObject.get(); + if (object) { + return object; + } + + if (auto* thisObject = jsDynamicCast(this)) { + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + object = thisObject->moduleRecord(globalObject)->getModuleNamespace(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + if (object) { + namespaceObject(vm, object); + } + } else { + RELEASE_ASSERT_NOT_REACHED_WITH_MESSAGE("NodeVMModule::namespaceObject called on an unsupported module type (%s)", info()->className.characters()); + } + + return object; +} + +JSC_DECLARE_CUSTOM_GETTER(jsNodeVmModuleGetterIdentifier); +JSC_DECLARE_HOST_FUNCTION(jsNodeVmModuleGetStatusCode); +JSC_DECLARE_HOST_FUNCTION(jsNodeVmModuleGetStatus); +JSC_DECLARE_HOST_FUNCTION(jsNodeVmModuleGetNamespace); +JSC_DECLARE_HOST_FUNCTION(jsNodeVmModuleGetError); +JSC_DECLARE_HOST_FUNCTION(jsNodeVmModuleInstantiate); +JSC_DECLARE_HOST_FUNCTION(jsNodeVmModuleEvaluate); +JSC_DECLARE_HOST_FUNCTION(jsNodeVmModuleGetModuleRequests); +JSC_DECLARE_HOST_FUNCTION(jsNodeVmModuleLink); +JSC_DECLARE_HOST_FUNCTION(jsNodeVmModuleCreateCachedData); +JSC_DECLARE_HOST_FUNCTION(jsNodeVmModuleSetExport); +JSC_DECLARE_HOST_FUNCTION(jsNodeVmModuleCreateModuleRecord); + +static const HashTableValue NodeVMModulePrototypeTableValues[] = { + { "identifier"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeVmModuleGetterIdentifier, nullptr } }, + { "getStatusCode"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsNodeVmModuleGetStatusCode, 0 } }, + { "getStatus"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsNodeVmModuleGetStatus, 0 } }, + { "getNamespace"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsNodeVmModuleGetNamespace, 0 } }, + { "getError"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsNodeVmModuleGetError, 0 } }, + { "instantiate"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsNodeVmModuleInstantiate, 0 } }, + { "evaluate"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsNodeVmModuleEvaluate, 2 } }, + { "getModuleRequests"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsNodeVmModuleGetModuleRequests, 0 } }, + { "link"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsNodeVmModuleLink, 2 } }, + { "createCachedData"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsNodeVmModuleCreateCachedData, 0 } }, + { "setExport"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsNodeVmModuleSetExport, 2 } }, + { "createModuleRecord"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsNodeVmModuleCreateModuleRecord, 0 } }, +}; + +NodeVMModulePrototype* NodeVMModulePrototype::create(VM& vm, Structure* structure) +{ + NodeVMModulePrototype* prototype = new (NotNull, allocateCell(vm)) NodeVMModulePrototype(vm, structure); + prototype->finishCreation(vm); + return prototype; +} + +Structure* NodeVMModulePrototype::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +NodeVMModulePrototype::NodeVMModulePrototype(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +void NodeVMModulePrototype::finishCreation(JSC::VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + reifyStaticProperties(vm, info(), NodeVMModulePrototypeTableValues, *this); + this->structure()->setMayBePrototype(true); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodeVmModuleGetterIdentifier, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, PropertyName propertyName)) +{ + auto* thisObject = jsCast(JSC::JSValue::decode(thisValue)); + return JSValue::encode(JSC::jsString(globalObject->vm(), thisObject->identifier())); +} + +JSC_DEFINE_HOST_FUNCTION(jsNodeVmModuleGetStatusCode, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto* thisObject = jsCast(callFrame->thisValue()); + return JSValue::encode(JSC::jsNumber(static_cast(thisObject->status()))); +} + +JSC_DEFINE_HOST_FUNCTION(jsNodeVmModuleGetStatus, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto* thisObject = jsCast(callFrame->thisValue()); + + using enum NodeVMModule::Status; + switch (thisObject->status()) { + case Unlinked: + return JSValue::encode(JSC::jsString(globalObject->vm(), WTF::String("unlinked"_s))); + case Linking: + return JSValue::encode(JSC::jsString(globalObject->vm(), WTF::String("linking"_s))); + case Linked: + return JSValue::encode(JSC::jsString(globalObject->vm(), WTF::String("linked"_s))); + case Evaluating: + return JSValue::encode(JSC::jsString(globalObject->vm(), WTF::String("evaluating"_s))); + case Evaluated: + return JSValue::encode(JSC::jsString(globalObject->vm(), WTF::String("evaluated"_s))); + case Errored: + return JSValue::encode(JSC::jsString(globalObject->vm(), WTF::String("errored"_s))); + default: + return JSC::encodedJSUndefined(); + } +} + +JSC_DEFINE_HOST_FUNCTION(jsNodeVmModuleGetNamespace, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto* thisObject = jsCast(callFrame->thisValue()); + return JSValue::encode(thisObject->namespaceObject(globalObject)); +} + +JSC_DEFINE_HOST_FUNCTION(jsNodeVmModuleGetError, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (auto* thisObject = jsCast(callFrame->thisValue())) { + if (JSC::Exception* exception = thisObject->evaluationException()) { + return JSValue::encode(exception->value()); + } + throwError(globalObject, scope, ErrorCode::ERR_VM_MODULE_STATUS, "Module status must be errored"_s); + return {}; + } + + throwTypeError(globalObject, scope, "This function must be called on a SourceTextModule or SyntheticModule"_s); + return {}; +} + +JSC_DEFINE_HOST_FUNCTION(jsNodeVmModuleGetModuleRequests, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto* thisObject = jsCast(callFrame->thisValue()); + + if (auto* sourceTextModule = jsDynamicCast(callFrame->thisValue())) { + sourceTextModule->ensureModuleRecord(globalObject); + } + + const WTF::Vector& requests = thisObject->moduleRequests(); + + JSArray* array = constructEmptyArray(globalObject, nullptr, requests.size()); + + for (unsigned i = 0; const NodeVMModuleRequest& request : requests) { + array->putDirectIndex(globalObject, i++, request.toJS(globalObject)); + } + + return JSValue::encode(array); +} + +JSC_DEFINE_HOST_FUNCTION(jsNodeVmModuleEvaluate, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue timeoutValue = callFrame->argument(0); + uint32_t timeout = 0; + if (timeoutValue.isUInt32()) { + timeout = timeoutValue.asUInt32(); + } + + JSValue breakOnSigintValue = callFrame->argument(1); + bool breakOnSigint = false; + if (breakOnSigintValue.isBoolean()) { + breakOnSigint = breakOnSigintValue.asBoolean(); + } + + if (auto* thisObject = jsDynamicCast(callFrame->thisValue())) { + return JSValue::encode(thisObject->evaluate(globalObject, timeout, breakOnSigint)); + // } else if (auto* thisObject = jsDynamicCast(callFrame->thisValue())) { + // return thisObject->link(globalObject, specifiers, moduleNatives); + } else { + throwTypeError(globalObject, scope, "This function must be called on a SourceTextModule or SyntheticModule"_s); + return {}; + } +} + +JSC_DEFINE_HOST_FUNCTION(jsNodeVmModuleLink, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSArray* specifiers = jsDynamicCast(callFrame->argument(0)); + JSArray* moduleNatives = jsDynamicCast(callFrame->argument(1)); + + if (!specifiers) { + return throwArgumentTypeError(*globalObject, scope, 0, "specifiers"_s, "Module"_s, "Module"_s, "Array"_s); + } + + if (!moduleNatives) { + return throwArgumentTypeError(*globalObject, scope, 1, "moduleNatives"_s, "Module"_s, "Module"_s, "Array"_s); + } + + if (auto* thisObject = jsDynamicCast(callFrame->thisValue())) { + return JSValue::encode(thisObject->link(globalObject, specifiers, moduleNatives, callFrame->argument(2))); + // return thisObject->link(globalObject, linker); + // } else if (auto* thisObject = jsDynamicCast(callFrame->thisValue())) { + // return thisObject->link(globalObject, specifiers, moduleNatives); + } else { + throwTypeError(globalObject, scope, "This function must be called on a SourceTextModule or SyntheticModule"_s); + return {}; + } +} + +JSC_DEFINE_HOST_FUNCTION(jsNodeVmModuleInstantiate, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + // auto* thisObject = jsCast(callFrame->thisValue()); + return JSC::encodedJSUndefined(); +} + +JSC_DEFINE_HOST_FUNCTION(jsNodeVmModuleSetExport, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + // auto* thisObject = jsCast(callFrame->thisValue()); + return JSC::encodedJSUndefined(); +} + +JSC_DEFINE_HOST_FUNCTION(jsNodeVmModuleCreateCachedData, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (auto* thisObject = jsDynamicCast(callFrame->thisValue())) { + return JSValue::encode(thisObject->cachedData(globalObject)); + } + + throwTypeError(globalObject, scope, "This function must be called on a SourceTextModule"_s); + return {}; +} + +JSC_DEFINE_HOST_FUNCTION(jsNodeVmModuleCreateModuleRecord, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto* thisObject = jsCast(callFrame->thisValue()); + return JSValue::encode(thisObject->createModuleRecord(globalObject)); +} + +template +void NodeVMModule::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* vmModule = jsCast(cell); + ASSERT_GC_OBJECT_INHERITS(vmModule, info()); + Base::visitChildren(vmModule, visitor); + + visitor.append(vmModule->m_namespaceObject); + visitor.append(vmModule->m_context); + + auto moduleNatives = vmModule->m_resolveCache.values(); + visitor.append(moduleNatives.begin(), moduleNatives.end()); +} + +DEFINE_VISIT_CHILDREN(NodeVMModule); + +static EncodedJSValue +constructModule(JSGlobalObject* globalObject, CallFrame* callFrame, JSValue newTarget = {}) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + ArgList args(callFrame); + + NodeVMModule* module = NodeVMModule::create(vm, globalObject, args); + + return JSValue::encode(module); +} + +JSC_DEFINE_HOST_FUNCTION(moduleConstructorCall, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + return constructModule(globalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(moduleConstructorConstruct, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + return constructModule(globalObject, callFrame, callFrame->newTarget()); +} + +NodeVMModuleConstructor* NodeVMModuleConstructor::create(VM& vm, JSGlobalObject* globalObject, Structure* structure, JSObject* prototype) +{ + NodeVMModuleConstructor* ptr = new (NotNull, allocateCell(vm)) NodeVMModuleConstructor(vm, structure); + ptr->finishCreation(vm, prototype); + return ptr; +} + +NodeVMModuleConstructor::NodeVMModuleConstructor(VM& vm, Structure* structure) + : NodeVMModuleConstructor::Base(vm, structure, moduleConstructorCall, moduleConstructorConstruct) +{ +} + +JSC::Structure* NodeVMModuleConstructor::createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) +{ + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::InternalFunctionType, Base::StructureFlags), info()); +} + +void NodeVMModuleConstructor::finishCreation(VM& vm, JSObject* prototype) +{ + Base::finishCreation(vm, 1, "Module"_s, PropertyAdditionMode::WithStructureTransition); + putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); + ASSERT(inherits(info())); +} + +const JSC::ClassInfo NodeVMModule::s_info = { "NodeVMSourceTextModule"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NodeVMSourceTextModule) }; +const JSC::ClassInfo NodeVMModulePrototype::s_info = { "NodeVMSourceTextModule"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NodeVMModulePrototype) }; +const JSC::ClassInfo NodeVMModuleConstructor::s_info = { "Module"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NodeVMModuleConstructor) }; + +} // namespace Bun diff --git a/src/bun.js/bindings/NodeVMModule.h b/src/bun.js/bindings/NodeVMModule.h new file mode 100644 index 0000000000..96f5c26d43 --- /dev/null +++ b/src/bun.js/bindings/NodeVMModule.h @@ -0,0 +1,123 @@ +#pragma once + +#include "NodeVM.h" + +#include "JavaScriptCore/AbstractModuleRecord.h" +#include "JavaScriptCore/JSModuleNamespaceObject.h" + +namespace Bun { + +class NodeVMSourceTextModule; + +class NodeVMModuleRequest final { +public: + NodeVMModuleRequest(WTF::String specifier, WTF::HashMap importAttributes = {}); + + JSArray* toJS(JSGlobalObject* globalObject) const; + void addImportAttribute(WTF::String key, WTF::String value); + + const WTF::String& specifier() const { return m_specifier; } + void specifier(WTF::String value) { m_specifier = value; } + const WTF::HashMap& importAttributes() const { return m_importAttributes; } + +private: + WTF::String m_specifier; + WTF::HashMap m_importAttributes; +}; + +class NodeVMModule : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + + enum class Status : uint8_t { + Unlinked, + Linking, + Linked, + Evaluating, + Evaluated, + Errored + }; + + enum class Type : uint8_t { + SourceText, + Synthetic, + }; + + static NodeVMModule* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, ArgList args); + + const WTF::String& identifier() const { return m_identifier; } + + Status status() const { return m_status; } + void status(Status value) { m_status = value; } + + JSModuleNamespaceObject* namespaceObject(JSC::JSGlobalObject* globalObject); + void namespaceObject(JSC::VM& vm, JSModuleNamespaceObject* value) { m_namespaceObject.set(vm, this, value); } + + const WTF::Vector& moduleRequests() const { return m_moduleRequests; } + void addModuleRequest(NodeVMModuleRequest request) { m_moduleRequests.append(WTFMove(request)); } + + // Purposely not virtual. Dispatches to the correct subclass. + bool finishInstantiate(JSC::JSGlobalObject* globalObject, WTF::Deque& stack, unsigned* dfsIndex); + + // Purposely not virtual. Dispatches to the correct subclass. + JSValue createModuleRecord(JSC::JSGlobalObject* globalObject); + + // Purposely not virtual. Dispatches to the correct subclass. + AbstractModuleRecord* moduleRecord(JSC::JSGlobalObject* globalObject); + +protected: + WTF::String m_identifier; + Status m_status = Status::Unlinked; + mutable WriteBarrier m_namespaceObject; + mutable WriteBarrier m_context; + WTF::Vector m_moduleRequests; + mutable WTF::HashMap> m_resolveCache; + + NodeVMModule(JSC::VM& vm, JSC::Structure* structure, WTF::String identifier, JSValue context); + + DECLARE_EXPORT_INFO; + DECLARE_VISIT_CHILDREN; +}; + +class NodeVMModulePrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + + static NodeVMModulePrototype* create(VM& vm, Structure* structure); + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(NodeVMModulePrototype, Base); + return &vm.plainObjectSpace(); + } + + static Structure* createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype); + +private: + NodeVMModulePrototype(VM& vm, Structure* structure); + + void finishCreation(VM& vm); +}; + +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(NodeVMModulePrototype, NodeVMModulePrototype::Base); + +class NodeVMModuleConstructor final : public JSC::InternalFunction { +public: + using Base = JSC::InternalFunction; + + DECLARE_EXPORT_INFO; + + static NodeVMModuleConstructor* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, JSC::JSObject* prototype); + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype); + +private: + NodeVMModuleConstructor(JSC::VM& vm, JSC::Structure* structure); + + void finishCreation(JSC::VM&, JSC::JSObject* prototype); +}; + +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(NodeVMModuleConstructor, JSC::InternalFunction); + +} // namespace Bun diff --git a/src/bun.js/bindings/NodeVMScript.cpp b/src/bun.js/bindings/NodeVMScript.cpp new file mode 100644 index 0000000000..1bcccff09c --- /dev/null +++ b/src/bun.js/bindings/NodeVMScript.cpp @@ -0,0 +1,646 @@ +#include "ErrorCode.h" +#include "JavaScriptCore/Completion.h" +#include "JavaScriptCore/JIT.h" +#include "JavaScriptCore/JSWeakMap.h" +#include "JavaScriptCore/JSWeakMapInlines.h" +#include "JavaScriptCore/ProgramCodeBlock.h" +#include "JavaScriptCore/SourceCodeKey.h" +#include "NodeVMScript.h" + +#include "../vm/SigintWatcher.h" + +namespace Bun { +using namespace NodeVM; + +bool ScriptOptions::fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg) +{ + bool any = BaseVMOptions::fromJS(globalObject, vm, scope, optionsArg); + RETURN_IF_EXCEPTION(scope, false); + + if (!optionsArg.isUndefined() && !optionsArg.isString()) { + JSObject* options = asObject(optionsArg); + + // Validate contextName and contextOrigin are strings + if (JSValue contextNameOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "contextName"_s))) { + if (!contextNameOpt.isUndefined() && !contextNameOpt.isString()) { + ERR::INVALID_ARG_TYPE(scope, globalObject, "options.contextName"_s, "string"_s, contextNameOpt); + return false; + } + any = true; + } + RETURN_IF_EXCEPTION(scope, false); + + if (JSValue contextOriginOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "contextOrigin"_s))) { + if (!contextOriginOpt.isUndefined() && !contextOriginOpt.isString()) { + ERR::INVALID_ARG_TYPE(scope, globalObject, "options.contextOrigin"_s, "string"_s, contextOriginOpt); + return false; + } + any = true; + } + RETURN_IF_EXCEPTION(scope, false); + + if (validateTimeout(globalObject, vm, scope, options, this->timeout)) { + RETURN_IF_EXCEPTION(scope, false); + any = true; + } + + if (validateProduceCachedData(globalObject, vm, scope, options, this->produceCachedData)) { + RETURN_IF_EXCEPTION(scope, false); + any = true; + } + + if (validateCachedData(globalObject, vm, scope, options, this->cachedData)) { + RETURN_IF_EXCEPTION(scope, false); + any = true; + } + } + + return any; +} + +static EncodedJSValue +constructScript(JSGlobalObject* globalObject, CallFrame* callFrame, JSValue newTarget = {}) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + ArgList args(callFrame); + JSValue sourceArg = args.at(0); + String sourceString = sourceArg.isUndefined() ? emptyString() : sourceArg.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, encodedJSUndefined()); + + JSValue optionsArg = args.at(1); + ScriptOptions options(""_s); + if (optionsArg.isString()) { + options.filename = optionsArg.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + } else if (!options.fromJS(globalObject, vm, scope, optionsArg)) { + RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined())); + } + + auto* zigGlobalObject = defaultGlobalObject(globalObject); + Structure* structure = zigGlobalObject->NodeVMScriptStructure(); + if (zigGlobalObject->NodeVMScript() != newTarget) [[unlikely]] { + auto scope = DECLARE_THROW_SCOPE(vm); + if (!newTarget) { + throwTypeError(globalObject, scope, "Class constructor Script cannot be invoked without 'new'"_s); + return {}; + } + + auto* functionGlobalObject = defaultGlobalObject(getFunctionRealm(globalObject, newTarget.getObject())); + RETURN_IF_EXCEPTION(scope, {}); + structure = InternalFunction::createSubclassStructure( + globalObject, newTarget.getObject(), functionGlobalObject->NodeVMScriptStructure()); + scope.release(); + } + + SourceCode source( + JSC::StringSourceProvider::create(sourceString, JSC::SourceOrigin(WTF::URL::fileURLWithFileSystemPath(options.filename)), options.filename, JSC::SourceTaintedOrigin::Untainted, TextPosition(options.lineOffset, options.columnOffset)), + options.lineOffset.zeroBasedInt(), options.columnOffset.zeroBasedInt()); + RETURN_IF_EXCEPTION(scope, {}); + + const bool produceCachedData = options.produceCachedData; + auto filename = options.filename; + + NodeVMScript* script = NodeVMScript::create(vm, globalObject, structure, source, WTFMove(options)); + + WTF::Vector& cachedData = script->cachedData(); + + if (!cachedData.isEmpty()) { + JSC::ProgramExecutable* executable = script->cachedExecutable(); + if (!executable) { + executable = script->createExecutable(); + } + ASSERT(executable); + + JSC::LexicallyScopedFeatures lexicallyScopedFeatures = globalObject->globalScopeExtension() ? JSC::TaintedByWithScopeLexicallyScopedFeature : JSC::NoLexicallyScopedFeatures; + JSC::SourceCodeKey key(source, {}, JSC::SourceCodeType::ProgramType, lexicallyScopedFeatures, JSC::JSParserScriptMode::Classic, JSC::DerivedContextType::None, JSC::EvalContextType::None, false, {}, std::nullopt); + Ref cachedBytecode = JSC::CachedBytecode::create(std::span(cachedData), nullptr, {}); + JSC::UnlinkedProgramCodeBlock* unlinkedBlock = JSC::decodeCodeBlock(vm, key, WTFMove(cachedBytecode)); + + if (!unlinkedBlock) { + script->cachedDataRejected(TriState::True); + } else { + JSC::JSScope* jsScope = globalObject->globalScope(); + JSC::CodeBlock* codeBlock = nullptr; + { + // JSC::ProgramCodeBlock::create() requires GC to be deferred. + DeferGC deferGC(vm); + codeBlock = JSC::ProgramCodeBlock::create(vm, executable, unlinkedBlock, jsScope); + } + JSC::CompilationResult compilationResult = JIT::compileSync(vm, codeBlock, JITCompilationEffort::JITCompilationCanFail); + if (compilationResult != JSC::CompilationResult::CompilationFailed) { + executable->installCode(codeBlock); + script->cachedDataRejected(TriState::False); + } else { + script->cachedDataRejected(TriState::True); + } + } + } else if (produceCachedData) { + script->cacheBytecode(); + // TODO(@heimskr): is there ever a case where bytecode production fails? + script->cachedDataProduced(true); + } + + return JSValue::encode(script); +} + +JSC_DEFINE_HOST_FUNCTION(scriptConstructorCall, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + return constructScript(globalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(scriptConstructorConstruct, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + return constructScript(globalObject, callFrame, callFrame->newTarget()); +} + +JSC::ProgramExecutable* NodeVMScript::createExecutable() +{ + VM& vm = JSC::getVM(globalObject()); + m_cachedExecutable.set(vm, this, JSC::ProgramExecutable::create(globalObject(), m_source)); + return m_cachedExecutable.get(); +} + +void NodeVMScript::cacheBytecode() +{ + if (!m_cachedExecutable) { + createExecutable(); + } + + m_cachedBytecode = getBytecode(globalObject(), m_cachedExecutable.get(), m_source); + m_cachedDataProduced = m_cachedBytecode != nullptr; +} + +JSC::JSUint8Array* NodeVMScript::getBytecodeBuffer() +{ + if (!m_options.produceCachedData) { + return nullptr; + } + + if (!m_cachedBytecodeBuffer) { + if (!m_cachedBytecode) { + cacheBytecode(); + } + + ASSERT(m_cachedBytecode); + + std::span bytes = m_cachedBytecode->span(); + m_cachedBytecodeBuffer.set(vm(), this, WebCore::createBuffer(globalObject(), bytes)); + } + + ASSERT(m_cachedBytecodeBuffer); + return m_cachedBytecodeBuffer.get(); +} + +DEFINE_VISIT_CHILDREN(NodeVMScript); + +template +void NodeVMScript::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + NodeVMScript* thisObject = jsCast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_cachedExecutable); + visitor.append(thisObject->m_cachedBytecodeBuffer); +} + +NodeVMScriptConstructor::NodeVMScriptConstructor(VM& vm, Structure* structure) + : NodeVMScriptConstructor::Base(vm, structure, scriptConstructorCall, scriptConstructorConstruct) +{ +} + +NodeVMScriptConstructor* NodeVMScriptConstructor::create(VM& vm, JSGlobalObject* globalObject, Structure* structure, JSObject* prototype) +{ + NodeVMScriptConstructor* ptr = new (NotNull, allocateCell(vm)) NodeVMScriptConstructor(vm, structure); + ptr->finishCreation(vm, prototype); + return ptr; +} + +void NodeVMScriptConstructor::finishCreation(VM& vm, JSObject* prototype) +{ + Base::finishCreation(vm, 1, "Script"_s, PropertyAdditionMode::WithStructureTransition); + putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); + ASSERT(inherits(info())); +} + +NodeVMScript* NodeVMScript::create(VM& vm, JSGlobalObject* globalObject, Structure* structure, SourceCode source, ScriptOptions options) +{ + NodeVMScript* ptr = new (NotNull, allocateCell(vm)) NodeVMScript(vm, structure, source, WTFMove(options)); + ptr->finishCreation(vm); + return ptr; +} + +void NodeVMScript::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +void NodeVMScript::destroy(JSCell* cell) +{ + static_cast(cell)->NodeVMScript::~NodeVMScript(); +} + +static bool checkForTermination(JSGlobalObject* globalObject, ThrowScope& scope, NodeVMScript* script, std::optional timeout) +{ + VM& vm = JSC::getVM(globalObject); + + if (vm.hasTerminationRequest()) { + vm.clearHasTerminationRequest(); + if (script->getSigintReceived()) { + script->setSigintReceived(false); + throwError(globalObject, scope, ErrorCode::ERR_SCRIPT_EXECUTION_INTERRUPTED, "Script execution was interrupted by `SIGINT`"_s); + } else if (timeout) { + throwError(globalObject, scope, ErrorCode::ERR_SCRIPT_EXECUTION_TIMEOUT, makeString("Script execution timed out after "_s, *timeout, "ms"_s)); + } else { + RELEASE_ASSERT_NOT_REACHED_WITH_MESSAGE("vm.Script terminated due neither to SIGINT nor to timeout"); + } + return true; + } + + return false; +} + +static void setupWatchdog(VM& vm, double timeout, double* oldTimeout, double* newTimeout) +{ + JSC::JSLockHolder locker(vm); + JSC::Watchdog& dog = vm.ensureWatchdog(); + dog.enteredVM(); + + Seconds oldLimit = dog.getTimeLimit(); + + if (oldTimeout) { + *oldTimeout = oldLimit.milliseconds(); + } + + if (oldLimit.isInfinity() || timeout < oldLimit.milliseconds()) { + dog.setTimeLimit(WTF::Seconds::fromMilliseconds(timeout)); + } else { + timeout = oldLimit.milliseconds(); + } + + if (newTimeout) { + *newTimeout = timeout; + } +} + +static JSC::EncodedJSValue runInContext(NodeVMGlobalObject* globalObject, NodeVMScript* script, JSObject* contextifiedObject, JSValue optionsArg, bool allowStringInPlaceOfOptions = false) +{ + VM& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + RunningScriptOptions options; + if (allowStringInPlaceOfOptions && optionsArg.isString()) { + options.filename = optionsArg.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + } else if (!options.fromJS(globalObject, vm, scope, optionsArg)) { + RETURN_IF_EXCEPTION(scope, {}); + options = {}; + } + + // Set the contextified object before evaluating + globalObject->setContextifiedObject(contextifiedObject); + + NakedPtr exception; + JSValue result {}; + auto run = [&] { + result = JSC::evaluate(globalObject, script->source(), globalObject, exception); + }; + + std::optional oldLimit, newLimit; + + if (options.timeout) { + setupWatchdog(vm, *options.timeout, &oldLimit.emplace(), &newLimit.emplace()); + } + + script->setSigintReceived(false); + + if (options.breakOnSigint) { + auto holder = SigintWatcher::hold(globalObject, script); + run(); + } else { + run(); + } + + if (options.timeout) { + vm.watchdog()->setTimeLimit(WTF::Seconds::fromMilliseconds(*oldLimit)); + } + + if (checkForTermination(globalObject, scope, script, newLimit)) { + return {}; + } + + script->setSigintReceived(false); + + if (exception) [[unlikely]] { + if (handleException(globalObject, vm, exception, scope)) { + return {}; + } + JSC::throwException(globalObject, scope, exception.get()); + return {}; + } + + return JSValue::encode(result); +} + +JSC_DEFINE_HOST_FUNCTION(scriptRunInThisContext, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue thisValue = callFrame->thisValue(); + auto* script = jsDynamicCast(thisValue); + if (!script) [[unlikely]] { + return ERR::INVALID_ARG_VALUE(scope, globalObject, "this"_s, thisValue, "must be a Script"_s); + } + + JSValue optionsArg = callFrame->argument(0); + + RunningScriptOptions options; + if (!options.fromJS(globalObject, vm, scope, optionsArg)) { + RETURN_IF_EXCEPTION(scope, {}); + options = {}; + } + + NakedPtr exception; + JSValue result {}; + auto run = [&] { + result = JSC::evaluate(globalObject, script->source(), globalObject, exception); + }; + + std::optional oldLimit, newLimit; + + if (options.timeout) { + setupWatchdog(vm, *options.timeout, &oldLimit.emplace(), &newLimit.emplace()); + } + + script->setSigintReceived(false); + + if (options.breakOnSigint) { + auto holder = SigintWatcher::hold(globalObject, script); + vm.ensureTerminationException(); + run(); + } else { + run(); + } + + if (options.timeout) { + vm.watchdog()->setTimeLimit(WTF::Seconds::fromMilliseconds(*oldLimit)); + } + + if (checkForTermination(globalObject, scope, script, newLimit)) { + return {}; + } + + script->setSigintReceived(false); + + if (exception) [[unlikely]] { + if (handleException(globalObject, vm, exception, scope)) { + return {}; + } + JSC::throwException(globalObject, scope, exception.get()); + return {}; + } + + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(result); +} + +JSC_DEFINE_CUSTOM_GETTER(scriptGetSourceMapURL, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValueEncoded, PropertyName)) +{ + VM& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue thisValue = JSValue::decode(thisValueEncoded); + auto* script = jsDynamicCast(thisValue); + if (!script) [[unlikely]] { + return ERR::INVALID_ARG_VALUE(scope, globalObject, "this"_s, thisValue, "must be a Script"_s); + } + + const auto& url = script->source().provider()->sourceMappingURLDirective(); + return JSValue::encode(jsString(vm, url)); +} + +JSC_DEFINE_CUSTOM_GETTER(scriptGetCachedData, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValueEncoded, PropertyName)) +{ + VM& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue thisValue = JSValue::decode(thisValueEncoded); + auto* script = jsDynamicCast(thisValue); + if (!script) [[unlikely]] { + return ERR::INVALID_ARG_VALUE(scope, globalObject, "this"_s, thisValue, "must be a Script"_s); + } + + if (auto* buffer = script->getBytecodeBuffer()) { + return JSValue::encode(buffer); + } + + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_CUSTOM_GETTER(scriptGetCachedDataProduced, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValueEncoded, PropertyName)) +{ + VM& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue thisValue = JSValue::decode(thisValueEncoded); + auto* script = jsDynamicCast(thisValue); + if (!script) [[unlikely]] { + return ERR::INVALID_ARG_VALUE(scope, globalObject, "this"_s, thisValue, "must be a Script"_s); + } + + return JSValue::encode(jsBoolean(script->cachedDataProduced())); +} + +JSC_DEFINE_CUSTOM_GETTER(scriptGetCachedDataRejected, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValueEncoded, PropertyName)) +{ + VM& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue thisValue = JSValue::decode(thisValueEncoded); + auto* script = jsDynamicCast(thisValue); + if (!script) [[unlikely]] { + return ERR::INVALID_ARG_VALUE(scope, globalObject, "this"_s, thisValue, "must be a Script"_s); + } + + switch (script->cachedDataRejected()) { + case TriState::True: + return JSValue::encode(jsBoolean(true)); + case TriState::False: + return JSValue::encode(jsBoolean(false)); + default: + return JSValue::encode(jsUndefined()); + } +} + +JSC_DEFINE_HOST_FUNCTION(scriptCreateCachedData, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue thisValue = callFrame->thisValue(); + auto* script = jsDynamicCast(thisValue); + if (!script) [[unlikely]] { + return ERR::INVALID_ARG_VALUE(scope, globalObject, "this"_s, thisValue, "must be a Script"_s); + } + + const JSC::SourceCode& source = script->source(); + return createCachedData(globalObject, source); +} + +JSC_DEFINE_HOST_FUNCTION(scriptRunInContext, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue thisValue = callFrame->thisValue(); + auto* script = jsDynamicCast(thisValue); + if (!script) [[unlikely]] { + return ERR::INVALID_ARG_VALUE(scope, globalObject, "this"_s, thisValue, "must be a Script"_s); + } + + ArgList args(callFrame); + JSValue contextArg = args.at(0); + NodeVMGlobalObject* nodeVmGlobalObject = getGlobalObjectFromContext(globalObject, contextArg, true); + RETURN_IF_EXCEPTION(scope, {}); + JSObject* context = asObject(contextArg); + ASSERT(nodeVmGlobalObject != nullptr); + + return runInContext(nodeVmGlobalObject, script, context, args.at(1)); +} + +JSC_DEFINE_HOST_FUNCTION(scriptRunInNewContext, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = JSC::getVM(globalObject); + NodeVMScript* script = jsDynamicCast(callFrame->thisValue()); + JSValue contextObjectValue = callFrame->argument(0); + // TODO: options + // JSValue optionsObjectValue = callFrame->argument(1); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (!script) { + throwTypeError(globalObject, scope, "this.runInContext is not a function"_s); + return {}; + } + + if (contextObjectValue.isUndefined()) { + contextObjectValue = JSC::constructEmptyObject(globalObject); + } + + if (!contextObjectValue || !contextObjectValue.isObject()) [[unlikely]] { + throwTypeError(globalObject, scope, "Context must be an object"_s); + return {}; + } + + // we don't care about options for now + // TODO: options + // bool didThrow = false; + + auto* zigGlobal = defaultGlobalObject(globalObject); + JSObject* context = asObject(contextObjectValue); + auto* targetContext = NodeVMGlobalObject::create(vm, + zigGlobal->NodeVMGlobalObjectStructure(), + {}); + + return runInContext(targetContext, script, context, callFrame->argument(1)); +} + +class NodeVMScriptPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + + static NodeVMScriptPrototype* create(VM& vm, JSGlobalObject* globalObject, Structure* structure) + { + NodeVMScriptPrototype* ptr = new (NotNull, allocateCell(vm)) NodeVMScriptPrototype(vm, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + template + static GCClient::IsoSubspace* subspaceFor(VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(NodeVMScriptPrototype, Base); + return &vm.plainObjectSpace(); + } + static Structure* createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) + { + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); + } + +private: + NodeVMScriptPrototype(VM& vm, Structure* structure) + : Base(vm, structure) + { + } + + void finishCreation(VM&); +}; +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(NodeVMScriptPrototype, NodeVMScriptPrototype::Base); + +static const struct HashTableValue scriptPrototypeTableValues[] = { + { "createCachedData"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, scriptCreateCachedData, 1 } }, + { "runInContext"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, scriptRunInContext, 2 } }, + { "runInNewContext"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, scriptRunInNewContext, 2 } }, + { "runInThisContext"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, scriptRunInThisContext, 2 } }, + { "sourceMapURL"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, scriptGetSourceMapURL, nullptr } }, + { "cachedData"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, scriptGetCachedData, nullptr } }, + { "cachedDataProduced"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, scriptGetCachedDataProduced, nullptr } }, + { "cachedDataRejected"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, scriptGetCachedDataRejected, nullptr } }, +}; + +void NodeVMScriptPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, NodeVMScript::info(), scriptPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +JSObject* NodeVMScript::createPrototype(VM& vm, JSGlobalObject* globalObject) +{ + return NodeVMScriptPrototype::create(vm, globalObject, NodeVMScriptPrototype::createStructure(vm, globalObject, globalObject->objectPrototype())); +} + +const ClassInfo NodeVMScript::s_info = { "Script"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NodeVMScript) }; +const ClassInfo NodeVMScriptPrototype::s_info = { "Script"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NodeVMScriptPrototype) }; +const ClassInfo NodeVMScriptConstructor::s_info = { "Script"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NodeVMScriptConstructor) }; + +bool RunningScriptOptions::fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg) +{ + bool any = BaseVMOptions::fromJS(globalObject, vm, scope, optionsArg); + RETURN_IF_EXCEPTION(scope, false); + + if (!optionsArg.isUndefined() && !optionsArg.isString()) { + JSObject* options = asObject(optionsArg); + + if (JSValue displayErrorsOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "displayErrors"_s))) { + RETURN_IF_EXCEPTION(scope, false); + if (!displayErrorsOpt.isUndefined()) { + if (!displayErrorsOpt.isBoolean()) { + ERR::INVALID_ARG_TYPE(scope, globalObject, "options.displayErrors"_s, "boolean"_s, displayErrorsOpt); + return false; + } + this->displayErrors = displayErrorsOpt.asBoolean(); + any = true; + } + } + + if (validateTimeout(globalObject, vm, scope, options, this->timeout)) { + RETURN_IF_EXCEPTION(scope, false); + any = true; + } + + if (JSValue breakOnSigintOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "breakOnSigint"_s))) { + RETURN_IF_EXCEPTION(scope, false); + if (!breakOnSigintOpt.isUndefined()) { + if (!breakOnSigintOpt.isBoolean()) { + ERR::INVALID_ARG_TYPE(scope, globalObject, "options.breakOnSigint"_s, "boolean"_s, breakOnSigintOpt); + return false; + } + this->breakOnSigint = breakOnSigintOpt.asBoolean(); + any = true; + } + } + } + + return any; +} + +} // namespace Bun diff --git a/src/bun.js/bindings/NodeVMScript.h b/src/bun.js/bindings/NodeVMScript.h new file mode 100644 index 0000000000..11e13ce844 --- /dev/null +++ b/src/bun.js/bindings/NodeVMScript.h @@ -0,0 +1,113 @@ +#pragma once + +#include "NodeVM.h" + +#include "../vm/SigintReceiver.h" + +namespace Bun { + +class ScriptOptions : public BaseVMOptions { +public: + std::optional timeout = std::nullopt; + bool produceCachedData = false; + WTF::Vector cachedData; + + using BaseVMOptions::BaseVMOptions; + + bool fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg); +}; + +class NodeVMScriptConstructor final : public JSC::InternalFunction { +public: + using Base = JSC::InternalFunction; + + static NodeVMScriptConstructor* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, JSC::JSObject* prototype); + + DECLARE_EXPORT_INFO; + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::InternalFunctionType, Base::StructureFlags), info()); + } + +private: + NodeVMScriptConstructor(JSC::VM& vm, JSC::Structure* structure); + + void finishCreation(JSC::VM&, JSC::JSObject* prototype); +}; + +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(NodeVMScriptConstructor, JSC::InternalFunction); + +class NodeVMScript final : public JSC::JSDestructibleObject, public SigintReceiver { +public: + using Base = JSC::JSDestructibleObject; + + static NodeVMScript* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, JSC::SourceCode source, ScriptOptions options); + + DECLARE_EXPORT_INFO; + template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForNodeVMScript.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForNodeVMScript = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForNodeVMScript.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForNodeVMScript = std::forward(space); }); + } + + static void destroy(JSC::JSCell*); + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + + static JSObject* createPrototype(VM& vm, JSGlobalObject* globalObject); + + JSC::ProgramExecutable* createExecutable(); + void cacheBytecode(); + JSC::JSUint8Array* getBytecodeBuffer(); + + const JSC::SourceCode& source() const { return m_source; } + WTF::Vector& cachedData() { return m_options.cachedData; } + RefPtr cachedBytecode() const { return m_cachedBytecode; } + JSC::ProgramExecutable* cachedExecutable() const { return m_cachedExecutable.get(); } + bool cachedDataProduced() const { return m_cachedDataProduced; } + void cachedDataProduced(bool value) { m_cachedDataProduced = value; } + TriState cachedDataRejected() const { return m_cachedDataRejected; } + void cachedDataRejected(TriState value) { m_cachedDataRejected = value; } + + DECLARE_VISIT_CHILDREN; + +private: + JSC::SourceCode m_source; + RefPtr m_cachedBytecode; + JSC::WriteBarrier m_cachedBytecodeBuffer; + JSC::WriteBarrier m_cachedExecutable; + ScriptOptions m_options; + bool m_cachedDataProduced = false; + TriState m_cachedDataRejected = TriState::Indeterminate; + + NodeVMScript(JSC::VM& vm, JSC::Structure* structure, JSC::SourceCode source, ScriptOptions options) + : Base(vm, structure) + , m_source(source) + , m_options(WTFMove(options)) + { + } + + void finishCreation(JSC::VM&); +}; + +class RunningScriptOptions : public BaseVMOptions { +public: + bool displayErrors = true; + std::optional timeout = std::nullopt; + bool breakOnSigint = false; + + using BaseVMOptions::BaseVMOptions; + + bool fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg); +}; + +} // namespace Bun diff --git a/src/bun.js/bindings/NodeVMSourceTextModule.cpp b/src/bun.js/bindings/NodeVMSourceTextModule.cpp new file mode 100644 index 0000000000..69a9032b41 --- /dev/null +++ b/src/bun.js/bindings/NodeVMSourceTextModule.cpp @@ -0,0 +1,461 @@ +#include "NodeVMSourceTextModule.h" + +#include "ErrorCode.h" +#include "JSDOMExceptionHandling.h" + +#include "wtf/Scope.h" + +#include "JavaScriptCore/JIT.h" +#include "JavaScriptCore/JSModuleRecord.h" +#include "JavaScriptCore/JSPromise.h" +#include "JavaScriptCore/JSSourceCode.h" +#include "JavaScriptCore/ModuleAnalyzer.h" +#include "JavaScriptCore/ModuleProgramCodeBlock.h" +#include "JavaScriptCore/Parser.h" +#include "JavaScriptCore/SourceCodeKey.h" +#include "JavaScriptCore/Watchdog.h" + +#include "../vm/SigintWatcher.h" + +namespace Bun { +using namespace NodeVM; + +NodeVMSourceTextModule* NodeVMSourceTextModule::create(VM& vm, JSGlobalObject* globalObject, ArgList args) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue identifierValue = args.at(0); + if (!identifierValue.isString()) { + throwArgumentTypeError(*globalObject, scope, 0, "identifier"_s, "Module"_s, "Module"_s, "string"_s); + return nullptr; + } + + JSValue contextValue = args.at(1); + if (contextValue.isUndefined()) { + contextValue = globalObject; + } else if (!contextValue.isObject()) { + throwArgumentTypeError(*globalObject, scope, 1, "context"_s, "Module"_s, "Module"_s, "object"_s); + return nullptr; + } + + JSValue sourceTextValue = args.at(2); + if (!sourceTextValue.isString()) { + throwArgumentTypeError(*globalObject, scope, 2, "sourceText"_s, "Module"_s, "Module"_s, "string"_s); + return nullptr; + } + + JSValue lineOffsetValue = args.at(3); + if (!lineOffsetValue.isUInt32AsAnyInt()) { + throwArgumentTypeError(*globalObject, scope, 3, "lineOffset"_s, "Module"_s, "Module"_s, "number"_s); + return nullptr; + } + + JSValue columnOffsetValue = args.at(4); + if (!columnOffsetValue.isUInt32AsAnyInt()) { + throwArgumentTypeError(*globalObject, scope, 4, "columnOffset"_s, "Module"_s, "Module"_s, "number"_s); + return nullptr; + } + + JSValue cachedDataValue = args.at(5); + WTF::Vector cachedData; + if (!cachedDataValue.isUndefined() && !extractCachedData(cachedDataValue, cachedData)) { + Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "options.cachedData"_s, "Buffer, TypedArray, or DataView"_s, cachedDataValue); + return nullptr; + } + + uint32_t lineOffset = lineOffsetValue.toUInt32(globalObject); + uint32_t columnOffset = columnOffsetValue.toUInt32(globalObject); + + Ref sourceProvider = StringSourceProvider::create(sourceTextValue.toWTFString(globalObject), SourceOrigin {}, String {}, SourceTaintedOrigin::Untainted, + TextPosition { OrdinalNumber::fromZeroBasedInt(lineOffset), OrdinalNumber::fromZeroBasedInt(columnOffset) }, SourceProviderSourceType::Module); + + SourceCode sourceCode(WTFMove(sourceProvider), lineOffset, columnOffset); + + auto* zigGlobalObject = defaultGlobalObject(globalObject); + NodeVMSourceTextModule* ptr = new (NotNull, allocateCell(vm)) NodeVMSourceTextModule(vm, zigGlobalObject->NodeVMSourceTextModuleStructure(), identifierValue.toWTFString(globalObject), contextValue, WTFMove(sourceCode)); + ptr->finishCreation(vm); + + if (cachedData.isEmpty()) { + return ptr; + } + + ModuleProgramExecutable* executable = ModuleProgramExecutable::tryCreate(globalObject, ptr->sourceCode()); + if (!executable) { + // If an exception is already being thrown, don't throw another one. + // ModuleProgramExecutable::tryCreate() sometimes throws on failure, but sometimes it doesn't. + if (!scope.exception()) { + throwSyntaxError(globalObject, scope, "Failed to create cached executable"_s); + } + return nullptr; + } + + ptr->m_cachedExecutable.set(vm, ptr, executable); + LexicallyScopedFeatures lexicallyScopedFeatures = globalObject->globalScopeExtension() ? TaintedByWithScopeLexicallyScopedFeature : NoLexicallyScopedFeatures; + SourceCodeKey key(ptr->sourceCode(), {}, SourceCodeType::ProgramType, lexicallyScopedFeatures, JSParserScriptMode::Classic, DerivedContextType::None, EvalContextType::None, false, {}, std::nullopt); + Ref cachedBytecode = CachedBytecode::create(std::span(cachedData), nullptr, {}); + UnlinkedModuleProgramCodeBlock* unlinkedBlock = decodeCodeBlock(vm, key, WTFMove(cachedBytecode)); + + if (unlinkedBlock) { + JSScope* jsScope = globalObject->globalScope(); + CodeBlock* codeBlock = nullptr; + { + // JSC::ProgramCodeBlock::create() requires GC to be deferred. + DeferGC deferGC(vm); + codeBlock = ModuleProgramCodeBlock::create(vm, executable, unlinkedBlock, jsScope); + } + if (codeBlock) { + CompilationResult compilationResult = JIT::compileSync(vm, codeBlock, JITCompilationEffort::JITCompilationCanFail); + if (compilationResult != CompilationResult::CompilationFailed) { + executable->installCode(codeBlock); + return ptr; + } + } + } + + throwError(globalObject, scope, ErrorCode::ERR_VM_MODULE_CACHED_DATA_REJECTED, "cachedData buffer was rejected"_s); + return nullptr; +} + +void NodeVMSourceTextModule::destroy(JSCell* cell) +{ + static_cast(cell)->NodeVMSourceTextModule::~NodeVMSourceTextModule(); +} + +JSValue NodeVMSourceTextModule::createModuleRecord(JSGlobalObject* globalObject) +{ + if (m_moduleRequestsArray) { + return m_moduleRequestsArray.get(); + } + + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + ParserError parserError; + + std::unique_ptr node = parseRootNode(vm, m_sourceCode, + ImplementationVisibility::Public, + JSParserBuiltinMode::NotBuiltin, + StrictModeLexicallyScopedFeature, + JSParserScriptMode::Module, + SourceParseMode::ModuleAnalyzeMode, + parserError); + + if (parserError.isValid()) { + throwException(globalObject, scope, parserError.toErrorObject(globalObject, m_sourceCode)); + return {}; + } + + ModuleAnalyzer analyzer(globalObject, Identifier::fromString(vm, m_identifier), m_sourceCode, node->varDeclarations(), node->lexicalVariables(), AllFeatures); + + RETURN_IF_EXCEPTION(scope, {}); + ASSERT(node != nullptr); + + JSModuleRecord* moduleRecord = nullptr; + + if (auto result = analyzer.analyze(*node)) { + moduleRecord = *result; + } else { + auto [type, message] = result.error(); + throwError(globalObject, scope, ErrorCode::ERR_VM_MODULE_LINK_FAILURE, message); + return {}; + } + + m_moduleRecord.set(vm, this, moduleRecord); + m_moduleRequests.clear(); + + const auto& requests = moduleRecord->requestedModules(); + + if (requests.isEmpty()) { + return constructEmptyArray(globalObject, nullptr, 0); + } + + JSArray* requestsArray = constructEmptyArray(globalObject, nullptr, requests.size()); + + const auto& builtinNames = WebCore::clientData(vm)->builtinNames(); + const Identifier& specifierIdentifier = builtinNames.specifierPublicName(); + const Identifier& attributesIdentifier = builtinNames.attributesPublicName(); + const Identifier& hostDefinedImportTypeIdentifier = builtinNames.hostDefinedImportTypePublicName(); + + WTF::Vector attributesNodes; + attributesNodes.reserveInitialCapacity(requests.size()); + + for (StatementNode* statement = node->statements()->firstStatement(); statement; statement = statement->next()) { + // Assumption: module declarations occur here in the same order they occur in `requestedModules`. + if (statement->isModuleDeclarationNode()) { + ModuleDeclarationNode* moduleDeclaration = static_cast(statement); + if (moduleDeclaration->isImportDeclarationNode()) { + ImportDeclarationNode* importDeclaration = static_cast(moduleDeclaration); + ASSERT_WITH_MESSAGE(attributesNodes.size() < requests.size(), "More attributes nodes than requests"); + ASSERT_WITH_MESSAGE(importDeclaration->moduleName()->moduleName().string().string() == WTF::String(*requests.at(attributesNodes.size()).m_specifier), "Module name mismatch"); + attributesNodes.append(importDeclaration->attributesList()); + } else if (moduleDeclaration->hasAttributesList()) { + // Necessary to make the indices of `attributesNodes` and `requests` match up + attributesNodes.append(nullptr); + } + } + } + + ASSERT_WITH_MESSAGE(attributesNodes.size() == requests.size(), "Attributes node count doesn't match request count (%zu != %zu)", attributesNodes.size(), requests.size()); + + for (unsigned i = 0; i < requests.size(); ++i) { + const auto& request = requests[i]; + + JSString* specifierValue = JSC::jsString(vm, WTF::String(*request.m_specifier)); + + JSObject* requestObject = constructEmptyObject(globalObject, globalObject->objectPrototype(), 2); + requestObject->putDirect(vm, specifierIdentifier, specifierValue); + + WTF::String attributesTypeString = "unknown"_str; + + WTF::HashMap attributeMap; + JSObject* attributesObject = constructEmptyObject(globalObject); + + if (request.m_attributes) { + JSValue attributesType {}; + switch (request.m_attributes->type()) { + using AttributeType = decltype(request.m_attributes->type()); + using enum AttributeType; + case None: + attributesTypeString = "none"_str; + attributesType = JSC::jsString(vm, attributesTypeString); + break; + case JavaScript: + attributesTypeString = "javascript"_str; + attributesType = JSC::jsString(vm, attributesTypeString); + break; + case WebAssembly: + attributesTypeString = "webassembly"_str; + attributesType = JSC::jsString(vm, attributesTypeString); + break; + case JSON: + attributesTypeString = "json"_str; + attributesType = JSC::jsString(vm, attributesTypeString); + break; + default: + attributesType = JSC::jsNumber(static_cast(request.m_attributes->type())); + break; + } + + attributeMap.set("type"_s, WTFMove(attributesTypeString)); + attributesObject->putDirect(vm, JSC::Identifier::fromString(vm, "type"_s), attributesType); + + if (const String& hostDefinedImportType = request.m_attributes->hostDefinedImportType(); !hostDefinedImportType.isEmpty()) { + attributesObject->putDirect(vm, hostDefinedImportTypeIdentifier, JSC::jsString(vm, hostDefinedImportType)); + attributeMap.set("hostDefinedImportType"_s, hostDefinedImportType); + } + } + + if (ImportAttributesListNode* attributesNode = attributesNodes.at(i)) { + for (auto [key, value] : attributesNode->attributes()) { + attributeMap.set(key->string(), value->string()); + attributesObject->putDirect(vm, *key, JSC::jsString(vm, value->string())); + } + } + + requestObject->putDirect(vm, attributesIdentifier, attributesObject); + addModuleRequest({ WTF::String(*request.m_specifier), WTFMove(attributeMap) }); + requestsArray->putDirectIndex(globalObject, i, requestObject); + } + + m_moduleRequestsArray.set(vm, this, requestsArray); + return requestsArray; +} + +void NodeVMSourceTextModule::ensureModuleRecord(JSGlobalObject* globalObject) +{ + if (!m_moduleRecord) { + createModuleRecord(globalObject); + } +} + +AbstractModuleRecord* NodeVMSourceTextModule::moduleRecord(JSGlobalObject* globalObject) +{ + ensureModuleRecord(globalObject); + return m_moduleRecord.get(); +} + +JSValue NodeVMSourceTextModule::link(JSGlobalObject* globalObject, JSArray* specifiers, JSArray* moduleNatives, JSValue scriptFetcher) +{ + const unsigned length = specifiers->getArrayLength(); + + ASSERT(length == moduleNatives->getArrayLength()); + + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (m_status != Status::Unlinked) { + throwError(globalObject, scope, ErrorCode::ERR_VM_MODULE_STATUS, "Module must be unlinked before linking"_s); + return {}; + } + + JSModuleRecord* record = m_moduleRecord.get(); + + if (length != 0) { + for (unsigned i = 0; i < length; i++) { + JSValue specifierValue = specifiers->getDirectIndex(globalObject, i); + JSValue moduleNativeValue = moduleNatives->getDirectIndex(globalObject, i); + + ASSERT(specifierValue.isString()); + ASSERT(moduleNativeValue.isObject()); + + WTF::String specifier = specifierValue.toWTFString(globalObject); + JSObject* moduleNative = moduleNativeValue.getObject(); + AbstractModuleRecord* resolvedRecord = jsCast(moduleNative)->moduleRecord(globalObject); + + record->setImportedModule(globalObject, Identifier::fromString(vm, specifier), resolvedRecord); + m_resolveCache.set(WTFMove(specifier), WriteBarrier { vm, this, moduleNative }); + } + } + + if (NodeVMGlobalObject* nodeVmGlobalObject = getGlobalObjectFromContext(globalObject, m_context.get(), false)) { + globalObject = nodeVmGlobalObject; + } + + Synchronousness sync = record->link(globalObject, scriptFetcher); + + RETURN_IF_EXCEPTION(scope, {}); + + if (sync == Synchronousness::Async) { + RELEASE_ASSERT_NOT_REACHED_WITH_MESSAGE("TODO(@heimskr): async module linking"); + } + + status(Status::Linked); + return JSC::jsUndefined(); +} + +JSValue NodeVMSourceTextModule::evaluate(JSGlobalObject* globalObject, uint32_t timeout, bool breakOnSigint) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (m_status != Status::Linked && m_status != Status::Evaluated && m_status != Status::Errored) { + throwError(globalObject, scope, ErrorCode::ERR_VM_MODULE_STATUS, "Module must be linked, evaluated or errored before evaluating"_s); + return {}; + } + + JSModuleRecord* record = m_moduleRecord.get(); + JSValue result {}; + + NodeVMGlobalObject* nodeVmGlobalObject = getGlobalObjectFromContext(globalObject, m_context.get(), false); + + if (nodeVmGlobalObject) { + globalObject = nodeVmGlobalObject; + } + + auto run = [&] { + status(Status::Evaluating); + + for (const auto& request : record->requestedModules()) { + if (auto iter = m_resolveCache.find(WTF::String(*request.m_specifier)); iter != m_resolveCache.end()) { + if (auto* dependency = jsDynamicCast(iter->value.get())) { + if (dependency->status() == Status::Linked) { + JSValue dependencyResult = dependency->evaluate(globalObject, timeout, breakOnSigint); + RELEASE_ASSERT_WITH_MESSAGE(jsDynamicCast(dependencyResult) == nullptr, "TODO(@heimskr): implement async support for node:vm SourceTextModule dependencies"); + } + } + } + } + + result = record->evaluate(globalObject, jsUndefined(), jsNumber(static_cast(JSGenerator::ResumeMode::NormalMode))); + }; + + setSigintReceived(false); + + if (timeout != 0) { + JSC::JSLockHolder locker(vm); + JSC::Watchdog& dog = vm.ensureWatchdog(); + dog.enteredVM(); + dog.setTimeLimit(WTF::Seconds::fromMilliseconds(timeout)); + } + + if (breakOnSigint) { + auto holder = SigintWatcher::hold(nodeVmGlobalObject, this); + run(); + } else { + run(); + } + + if (timeout != 0) { + vm.watchdog()->setTimeLimit(JSC::Watchdog::noTimeLimit); + } + + if (vm.hasPendingTerminationException()) { + scope.clearException(); + vm.clearHasTerminationRequest(); + if (getSigintReceived()) { + setSigintReceived(false); + throwError(globalObject, scope, ErrorCode::ERR_SCRIPT_EXECUTION_INTERRUPTED, "Script execution was interrupted by `SIGINT`"_s); + } else { + throwError(globalObject, scope, ErrorCode::ERR_SCRIPT_EXECUTION_TIMEOUT, makeString("Script execution timed out after "_s, timeout, "ms"_s)); + } + } else { + setSigintReceived(false); + } + + if (JSC::Exception* exception = scope.exception()) { + status(Status::Errored); + m_evaluationException.set(vm, this, exception); + return {}; + } + + status(Status::Evaluated); + return result; +} + +RefPtr NodeVMSourceTextModule::bytecode(JSGlobalObject* globalObject) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (!m_bytecode) { + if (!m_cachedExecutable) { + ModuleProgramExecutable* executable = ModuleProgramExecutable::tryCreate(globalObject, m_sourceCode); + if (!executable) { + if (!scope.exception()) { + throwSyntaxError(globalObject, scope, "Failed to create cached executable"_s); + } + return nullptr; + } + m_cachedExecutable.set(vm, this, executable); + } + m_bytecode = getBytecode(globalObject, m_cachedExecutable.get(), m_sourceCode); + } + + return m_bytecode; +} + +JSUint8Array* NodeVMSourceTextModule::cachedData(JSGlobalObject* globalObject) +{ + if (!m_cachedBytecodeBuffer) { + RefPtr cachedBytecode = bytecode(globalObject); + std::span bytes = cachedBytecode->span(); + m_cachedBytecodeBuffer.set(vm(), this, WebCore::createBuffer(globalObject, bytes)); + } + + return m_cachedBytecodeBuffer.get(); +} + +JSObject* NodeVMSourceTextModule::createPrototype(VM& vm, JSGlobalObject* globalObject) +{ + return NodeVMModulePrototype::create(vm, NodeVMModulePrototype::createStructure(vm, globalObject, globalObject->objectPrototype())); +} + +template +void NodeVMSourceTextModule::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* vmModule = jsCast(cell); + ASSERT_GC_OBJECT_INHERITS(vmModule, info()); + Base::visitChildren(vmModule, visitor); + + visitor.append(vmModule->m_moduleRecord); + visitor.append(vmModule->m_moduleRequestsArray); + visitor.append(vmModule->m_cachedExecutable); + visitor.append(vmModule->m_cachedBytecodeBuffer); + visitor.append(vmModule->m_evaluationException); +} + +DEFINE_VISIT_CHILDREN(NodeVMSourceTextModule); + +const JSC::ClassInfo NodeVMSourceTextModule::s_info = { "NodeVMSourceTextModule"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NodeVMSourceTextModule) }; + +} // namespace Bun diff --git a/src/bun.js/bindings/NodeVMSourceTextModule.h b/src/bun.js/bindings/NodeVMSourceTextModule.h new file mode 100644 index 0000000000..6a58f11239 --- /dev/null +++ b/src/bun.js/bindings/NodeVMSourceTextModule.h @@ -0,0 +1,73 @@ +#pragma once + +#include "NodeVM.h" +#include "NodeVMModule.h" + +#include "../vm/SigintReceiver.h" + +namespace Bun { + +class NodeVMSourceTextModule final : public NodeVMModule, public SigintReceiver { +public: + using Base = NodeVMModule; + + static NodeVMSourceTextModule* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, ArgList args); + + template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForNodeVMSourceTextModule.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForNodeVMSourceTextModule = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForNodeVMSourceTextModule.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForNodeVMSourceTextModule = std::forward(space); }); + } + + static JSObject* createPrototype(VM& vm, JSGlobalObject* globalObject); + static void destroy(JSC::JSCell* cell); + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + + JSValue createModuleRecord(JSGlobalObject* globalObject); + void ensureModuleRecord(JSGlobalObject* globalObject); + bool hasModuleRecord() const { return !!m_moduleRecord; } + AbstractModuleRecord* moduleRecord(JSGlobalObject* globalObject); + JSValue link(JSGlobalObject* globalObject, JSArray* specifiers, JSArray* moduleNatives, JSValue scriptFetcher); + JSValue evaluate(JSGlobalObject* globalObject, uint32_t timeout, bool breakOnSigint); + RefPtr bytecode(JSGlobalObject* globalObject); + JSUint8Array* cachedData(JSGlobalObject* globalObject); + Exception* evaluationException() const { return m_evaluationException.get(); } + + const SourceCode& sourceCode() const { return m_sourceCode; } + ModuleProgramExecutable* cachedExecutable() const { return m_cachedExecutable.get(); } + + DECLARE_EXPORT_INFO; + DECLARE_VISIT_CHILDREN; + +private: + WriteBarrier m_moduleRecord; + WriteBarrier m_moduleRequestsArray; + WriteBarrier m_cachedExecutable; + WriteBarrier m_cachedBytecodeBuffer; + WriteBarrier m_evaluationException; + RefPtr m_bytecode; + SourceCode m_sourceCode; + + NodeVMSourceTextModule(JSC::VM& vm, JSC::Structure* structure, WTF::String identifier, JSValue context, SourceCode sourceCode) + : Base(vm, structure, WTFMove(identifier), context) + , m_sourceCode(WTFMove(sourceCode)) + { + } + + void finishCreation(JSC::VM& vm) + { + Base::finishCreation(vm); + ASSERT(inherits(info())); + } +}; + +} // namespace Bun diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 4e49fa14ad..ed83476f35 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -1317,7 +1317,7 @@ void GlobalObject::promiseRejectionTracker(JSGlobalObject* obj, JSC::JSPromise* // obj, prom, reject == JSC::JSPromiseRejectionOperation::Reject ? 0 : 1); // Do this in C++ for now - auto* globalObj = reinterpret_cast(obj); + auto* globalObj = static_cast(obj); switch (operation) { case JSPromiseRejectionOperation::Reject: globalObj->m_aboutToBeNotifiedRejectedPromises.append(JSC::Strong(obj->vm(), promise)); @@ -1759,7 +1759,7 @@ extern "C" JSC::EncodedJSValue Bun__createUint8ArrayForCopy(JSC::JSGlobalObject* VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - auto* subclassStructure = isBuffer ? reinterpret_cast(globalObject)->JSBufferSubclassStructure() : globalObject->typedArrayStructureWithTypedArrayType(); + auto* subclassStructure = isBuffer ? static_cast(globalObject)->JSBufferSubclassStructure() : globalObject->typedArrayStructureWithTypedArrayType(); JSC::JSUint8Array* array = JSC::JSUint8Array::createUninitialized(globalObject, subclassStructure, len); if (!array) [[unlikely]] { @@ -1869,7 +1869,7 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionDispatchEvent, (JSGlobalObject * lexicalGloba JSC_DEFINE_CUSTOM_GETTER(getterSubtleCrypto, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) { - return JSValue::encode(reinterpret_cast(lexicalGlobalObject)->subtleCrypto()); + return JSValue::encode(static_cast(lexicalGlobalObject)->subtleCrypto()); } extern "C" JSC::EncodedJSValue ExpectMatcherUtils_createSigleton(JSC::JSGlobalObject* lexicalGlobalObject); @@ -2235,7 +2235,7 @@ static inline JSC::EncodedJSValue ZigGlobalObject__readableStreamToArrayBufferBo extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToArrayBuffer(Zig::GlobalObject* globalObject, JSC::EncodedJSValue readableStreamValue); extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToArrayBuffer(Zig::GlobalObject* globalObject, JSC::EncodedJSValue readableStreamValue) { - return ZigGlobalObject__readableStreamToArrayBufferBody(reinterpret_cast(globalObject), readableStreamValue); + return ZigGlobalObject__readableStreamToArrayBufferBody(static_cast(globalObject), readableStreamValue); } extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToBytes(Zig::GlobalObject* globalObject, JSC::EncodedJSValue readableStreamValue); @@ -2378,7 +2378,7 @@ JSC_DEFINE_HOST_FUNCTION(functionReadableStreamToArrayBuffer, (JSGlobalObject * } auto readableStreamValue = callFrame->uncheckedArgument(0); - return ZigGlobalObject__readableStreamToArrayBufferBody(reinterpret_cast(globalObject), JSValue::encode(readableStreamValue)); + return ZigGlobalObject__readableStreamToArrayBufferBody(static_cast(globalObject), JSValue::encode(readableStreamValue)); } JSC_DECLARE_HOST_FUNCTION(functionReadableStreamToBytes); @@ -2393,7 +2393,7 @@ JSC_DEFINE_HOST_FUNCTION(functionReadableStreamToBytes, (JSGlobalObject * global } auto readableStreamValue = callFrame->uncheckedArgument(0); - return ZigGlobalObject__readableStreamToBytes(reinterpret_cast(globalObject), JSValue::encode(readableStreamValue)); + return ZigGlobalObject__readableStreamToBytes(static_cast(globalObject), JSValue::encode(readableStreamValue)); } JSC_DEFINE_HOST_FUNCTION(jsFunctionPerformMicrotask, (JSGlobalObject * globalObject, CallFrame* callframe)) @@ -2531,7 +2531,7 @@ void GlobalObject::createCallSitesFromFrames(Zig::GlobalObject* globalObject, JS JSC_DEFINE_HOST_FUNCTION(errorConstructorFuncAppendStackTrace, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) { - GlobalObject* globalObject = reinterpret_cast(lexicalGlobalObject); + GlobalObject* globalObject = static_cast(lexicalGlobalObject); auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); @@ -2615,7 +2615,7 @@ JSC_DEFINE_CUSTOM_SETTER(errorInstanceLazyStackCustomSetter, (JSGlobalObject * g JSC_DEFINE_HOST_FUNCTION(errorConstructorFuncCaptureStackTrace, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) { - GlobalObject* globalObject = reinterpret_cast(lexicalGlobalObject); + GlobalObject* globalObject = static_cast(lexicalGlobalObject); auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); @@ -2837,7 +2837,7 @@ void GlobalObject::finishCreation(VM& vm) m_commonJSModuleObjectStructure.initLater( [](const Initializer& init) { - init.set(Bun::createCommonJSModuleStructure(reinterpret_cast(init.owner))); + init.set(Bun::createCommonJSModuleStructure(static_cast(init.owner))); }); m_JSSocketAddressDTOStructure.initLater( @@ -2883,7 +2883,7 @@ void GlobalObject::finishCreation(VM& vm) [](const JSC::LazyProperty::Initializer& init) { init.set( createMemoryFootprintStructure( - init.vm, reinterpret_cast(init.owner))); + init.vm, static_cast(init.owner))); }); m_errorConstructorPrepareStackTraceInternalValue.initLater( @@ -2906,14 +2906,14 @@ void GlobalObject::finishCreation(VM& vm) m_JSBufferSubclassStructure.initLater( [](const Initializer& init) { - auto* globalObject = reinterpret_cast(init.owner); + auto* globalObject = static_cast(init.owner); auto* baseStructure = globalObject->typedArrayStructureWithTypedArrayType(); JSC::Structure* subclassStructure = JSC::InternalFunction::createSubclassStructure(globalObject, globalObject->JSBufferConstructor(), baseStructure); init.set(subclassStructure); }); m_JSResizableOrGrowableSharedBufferSubclassStructure.initLater( [](const Initializer& init) { - auto* globalObject = reinterpret_cast(init.owner); + auto* globalObject = static_cast(init.owner); auto* baseStructure = globalObject->resizableOrGrowableSharedTypedArrayStructureWithTypedArrayType(); JSC::Structure* subclassStructure = JSC::InternalFunction::createSubclassStructure(globalObject, globalObject->JSBufferConstructor(), baseStructure); init.set(subclassStructure); @@ -3045,17 +3045,17 @@ void GlobalObject::finishCreation(VM& vm) m_ServerRouteListStructure.initLater( [](const JSC::LazyProperty::Initializer& init) { - init.set(Bun::createServerRouteListStructure(init.vm, reinterpret_cast(init.owner))); + init.set(Bun::createServerRouteListStructure(init.vm, static_cast(init.owner))); }); m_JSBunRequestParamsPrototype.initLater( [](const JSC::LazyProperty::Initializer& init) { - init.set(Bun::createJSBunRequestParamsPrototype(init.vm, reinterpret_cast(init.owner))); + init.set(Bun::createJSBunRequestParamsPrototype(init.vm, static_cast(init.owner))); }); m_JSBunRequestStructure.initLater( [](const JSC::LazyProperty::Initializer& init) { - init.set(Bun::createJSBunRequestStructure(init.vm, reinterpret_cast(init.owner))); + init.set(Bun::createJSBunRequestStructure(init.vm, static_cast(init.owner))); }); m_NapiHandleScopeImplStructure.initLater([](const JSC::LazyProperty::Initializer& init) { @@ -3078,7 +3078,7 @@ void GlobalObject::finishCreation(VM& vm) m_subtleCryptoObject.initLater( [](const JSC::LazyProperty::Initializer& init) { - auto& global = *reinterpret_cast(init.owner); + auto& global = *static_cast(init.owner); if (!global.m_subtleCrypto) { global.m_subtleCrypto = &WebCore::SubtleCrypto::create(global.scriptExecutionContext()).leakRef(); @@ -3124,13 +3124,13 @@ void GlobalObject::finishCreation(VM& vm) m_performanceObject.initLater( [](const JSC::LazyProperty::Initializer& init) { - auto* globalObject = reinterpret_cast(init.owner); + auto* globalObject = static_cast(init.owner); init.set(toJS(init.owner, globalObject, globalObject->performance().get()).getObject()); }); m_processEnvObject.initLater( [](const JSC::LazyProperty::Initializer& init) { - init.set(Bun::createEnvironmentVariablesMap(reinterpret_cast(init.owner)).getObject()); + init.set(Bun::createEnvironmentVariablesMap(static_cast(init.owner)).getObject()); }); m_processObject.initLater( @@ -3289,7 +3289,7 @@ void GlobalObject::finishCreation(VM& vm) m_JSCryptoKey.initLater( [](const JSC::LazyProperty::Initializer& init) { - Zig::GlobalObject* globalObject = reinterpret_cast(init.owner); + Zig::GlobalObject* globalObject = static_cast(init.owner); auto* prototype = JSCryptoKey::createPrototype(init.vm, *globalObject); auto* structure = JSCryptoKey::createStructure(init.vm, init.owner, JSValue(prototype)); init.set(structure); @@ -3547,7 +3547,7 @@ JSC_DEFINE_CUSTOM_GETTER(functionLazyNavigatorGetter, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) { - return JSC::JSValue::encode(reinterpret_cast(globalObject)->navigatorObject()); + return JSC::JSValue::encode(static_cast(globalObject)->navigatorObject()); } JSC::GCClient::IsoSubspace* GlobalObject::subspaceForImpl(JSC::VM& vm) @@ -3960,7 +3960,7 @@ void GlobalObject::reload() extern "C" void JSC__JSGlobalObject__reload(JSC::JSGlobalObject* arg0) { - Zig::GlobalObject* globalObject = reinterpret_cast(arg0); + Zig::GlobalObject* globalObject = static_cast(arg0); globalObject->reload(); } @@ -3976,7 +3976,7 @@ JSC::Identifier GlobalObject::moduleLoaderResolve(JSGlobalObject* jsGlobalObject JSModuleLoader* loader, JSValue key, JSValue referrer, JSValue origin) { - Zig::GlobalObject* globalObject = reinterpret_cast(jsGlobalObject); + Zig::GlobalObject* globalObject = static_cast(jsGlobalObject); ErrorableString res; res.success = false; @@ -4032,7 +4032,7 @@ JSC::JSInternalPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* j JSValue parameters, const SourceOrigin& sourceOrigin) { - auto* globalObject = reinterpret_cast(jsGlobalObject); + auto* globalObject = static_cast(jsGlobalObject); auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); JSC::Identifier resolvedIdentifier; @@ -4197,7 +4197,7 @@ JSC::JSInternalPromise* GlobalObject::moduleLoaderFetch(JSGlobalObject* globalOb memset(&res.result, 0, sizeof res.result); JSValue result = Bun::fetchESMSourceCodeAsync( - reinterpret_cast(globalObject), + static_cast(globalObject), moduleKeyJS, &res, &moduleKeyBun, diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index bef364cc6f..1713e377e2 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -189,10 +189,10 @@ public: static void reportUncaughtExceptionAtEventLoop(JSGlobalObject*, JSC::Exception*); static JSGlobalObject* deriveShadowRealmGlobalObject(JSGlobalObject* globalObject); static JSC::JSInternalPromise* moduleLoaderImportModule(JSGlobalObject*, JSC::JSModuleLoader*, JSC::JSString* moduleNameValue, JSC::JSValue parameters, const JSC::SourceOrigin&); - static JSC::Identifier moduleLoaderResolve(JSGlobalObject*, JSC::JSModuleLoader*, JSC::JSValue keyValue, JSC::JSValue referrerValue, JSC::JSValue); - static JSC::JSInternalPromise* moduleLoaderFetch(JSGlobalObject*, JSC::JSModuleLoader*, JSC::JSValue, JSC::JSValue, JSC::JSValue); - static JSC::JSObject* moduleLoaderCreateImportMetaProperties(JSGlobalObject*, JSC::JSModuleLoader*, JSC::JSValue, JSC::JSModuleRecord*, JSC::JSValue); - static JSC::JSValue moduleLoaderEvaluate(JSGlobalObject*, JSC::JSModuleLoader*, JSC::JSValue, JSC::JSValue, JSC::JSValue, JSC::JSValue, JSC::JSValue); + static JSC::Identifier moduleLoaderResolve(JSGlobalObject*, JSC::JSModuleLoader*, JSC::JSValue key, JSC::JSValue referrer, JSC::JSValue origin); + static JSC::JSInternalPromise* moduleLoaderFetch(JSGlobalObject*, JSC::JSModuleLoader*, JSC::JSValue key, JSC::JSValue parameters, JSC::JSValue script); + static JSC::JSObject* moduleLoaderCreateImportMetaProperties(JSGlobalObject*, JSC::JSModuleLoader*, JSC::JSValue key, JSC::JSModuleRecord*, JSC::JSValue val); + static JSC::JSValue moduleLoaderEvaluate(JSGlobalObject*, JSC::JSModuleLoader*, JSValue key, JSValue moduleRecordValue, JSValue scriptFetcher, JSValue sentValue, JSValue resumeMode); static ScriptExecutionStatus scriptExecutionStatus(JSGlobalObject*, JSObject*); static void promiseRejectionTracker(JSGlobalObject*, JSC::JSPromise*, JSC::JSPromiseRejectionOperation); @@ -250,6 +250,14 @@ public: JSC::JSObject* NodeVMScript() const { return m_NodeVMScriptClassStructure.constructorInitializedOnMainThread(this); } JSC::JSValue NodeVMScriptPrototype() const { return m_NodeVMScriptClassStructure.prototypeInitializedOnMainThread(this); } + JSC::Structure* NodeVMSourceTextModuleStructure() const { return m_NodeVMSourceTextModuleClassStructure.getInitializedOnMainThread(this); } + JSC::JSObject* NodeVMSourceTextModule() const { return m_NodeVMSourceTextModuleClassStructure.constructorInitializedOnMainThread(this); } + JSC::JSValue NodeVMSourceTextModulePrototype() const { return m_NodeVMSourceTextModuleClassStructure.prototypeInitializedOnMainThread(this); } + + JSC::Structure* NodeVMSyntheticModuleStructure() const { return m_NodeVMSyntheticModuleClassStructure.getInitializedOnMainThread(this); } + JSC::JSObject* NodeVMSyntheticModule() const { return m_NodeVMSyntheticModuleClassStructure.constructorInitializedOnMainThread(this); } + JSC::JSValue NodeVMSyntheticModulePrototype() const { return m_NodeVMSyntheticModuleClassStructure.prototypeInitializedOnMainThread(this); } + JSC::JSMap* readableStreamNativeMap() const { return m_lazyReadableStreamPrototypeMap.getInitializedOnMainThread(this); } JSC::JSMap* requireMap() const { return m_requireMap.getInitializedOnMainThread(this); } JSC::JSMap* esmRegistryMap() const { return m_esmRegistryMap.getInitializedOnMainThread(this); } @@ -519,6 +527,8 @@ public: V(private, LazyClassStructure, m_callSiteStructure) \ V(public, LazyClassStructure, m_JSBufferClassStructure) \ V(public, LazyClassStructure, m_NodeVMScriptClassStructure) \ + V(public, LazyClassStructure, m_NodeVMSourceTextModuleClassStructure) \ + V(public, LazyClassStructure, m_NodeVMSyntheticModuleClassStructure) \ V(public, LazyClassStructure, m_JSX509CertificateClassStructure) \ V(public, LazyClassStructure, m_JSSignClassStructure) \ V(public, LazyClassStructure, m_JSVerifyClassStructure) \ diff --git a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h index bd97f654e9..49e562de5c 100644 --- a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h @@ -35,7 +35,10 @@ public: std::unique_ptr m_clientSubspaceForNapiExternal; std::unique_ptr m_clientSubspaceForRequireResolveFunction; std::unique_ptr m_clientSubspaceForBundlerPlugin; + std::unique_ptr m_clientSubspaceForNodeVMGlobalObject; std::unique_ptr m_clientSubspaceForNodeVMScript; + std::unique_ptr m_clientSubspaceForNodeVMSourceTextModule; + std::unique_ptr m_clientSubspaceForNodeVMSyntheticModule; std::unique_ptr m_clientSubspaceForJSCommonJSModule; std::unique_ptr m_clientSubspaceForJSCommonJSExtensions; std::unique_ptr m_clientSubspaceForJSMockImplementation; @@ -64,7 +67,6 @@ public: std::unique_ptr m_clientSubspaceForFunctionTemplate; std::unique_ptr m_clientSubspaceForV8Function; std::unique_ptr m_clientSubspaceForJSNodeHTTPServerSocket; - std::unique_ptr m_clientSubspaceForNodeVMGlobalObject; std::unique_ptr m_clientSubspaceForJSS3Bucket; std::unique_ptr m_clientSubspaceForJSS3File; std::unique_ptr m_clientSubspaceForJSX509Certificate; diff --git a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h index a7b07760f9..bce6f1bef4 100644 --- a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h @@ -35,7 +35,10 @@ public: std::unique_ptr m_subspaceForImportMeta; std::unique_ptr m_subspaceForRequireResolveFunction; std::unique_ptr m_subspaceForBundlerPlugin; + std::unique_ptr m_subspaceForNodeVMGlobalObject; std::unique_ptr m_subspaceForNodeVMScript; + std::unique_ptr m_subspaceForNodeVMSourceTextModule; + std::unique_ptr m_subspaceForNodeVMSyntheticModule; std::unique_ptr m_subspaceForJSCommonJSModule; std::unique_ptr m_subspaceForJSCommonJSExtensions; std::unique_ptr m_subspaceForJSMockImplementation; @@ -61,7 +64,6 @@ public: std::unique_ptr m_subspaceForJSMIMEParams; std::unique_ptr m_subspaceForV8Function; std::unique_ptr m_subspaceForJSNodeHTTPServerSocket; - std::unique_ptr m_subspaceForNodeVMGlobalObject; std::unique_ptr m_subspaceForJSS3Bucket; std::unique_ptr m_subspaceForJSS3File; std::unique_ptr m_subspaceForJSX509Certificate; diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts index 518d81b91f..2f389fc47f 100644 --- a/src/js/builtins.d.ts +++ b/src/js/builtins.d.ts @@ -787,6 +787,12 @@ declare function $ERR_HTTP_BODY_NOT_ALLOWED(): Error; declare function $ERR_HTTP_SOCKET_ASSIGNED(): Error; declare function $ERR_DIR_CLOSED(): Error; declare function $ERR_INVALID_MIME_SYNTAX(production: string, str: string, invalidIndex: number | -1): TypeError; +declare function $ERR_VM_MODULE_STATUS(reason: string): Error; +declare function $ERR_VM_MODULE_ALREADY_LINKED(): Error; +declare function $ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA(): Error; +declare function $ERR_VM_MODULE_NOT_MODULE(): Error; +declare function $ERR_VM_MODULE_DIFFERENT_CONTEXT(): Error; +declare function $ERR_VM_MODULE_LINK_FAILURE(message: string, cause: Error): Error; /** * Convert a function to a class-like object. diff --git a/src/js/builtins/BunBuiltinNames.h b/src/js/builtins/BunBuiltinNames.h index c505c166dc..0b71dee346 100644 --- a/src/js/builtins/BunBuiltinNames.h +++ b/src/js/builtins/BunBuiltinNames.h @@ -49,6 +49,7 @@ using namespace JSC; macro(assignToStream) \ macro(associatedReadableByteStreamController) \ macro(atimeMs) \ + macro(attributes) \ macro(autoAllocateChunkSize) \ macro(backpressure) \ macro(backpressureChangePromise) \ @@ -132,6 +133,7 @@ using namespace JSC; macro(headers) \ macro(highWaterMark) \ macro(host) \ + macro(hostDefinedImportType) \ macro(hostname) \ macro(href) \ macro(httpOnly) \ @@ -234,6 +236,7 @@ using namespace JSC; macro(signal) \ macro(sink) \ macro(size) \ + macro(specifier) \ macro(start) \ macro(startAlgorithm) \ macro(startConsumingStream) \ diff --git a/src/js/internal/primordials.js b/src/js/internal/primordials.js index d478237597..8eb8876333 100644 --- a/src/js/internal/primordials.js +++ b/src/js/internal/primordials.js @@ -94,7 +94,29 @@ const arrayToSafePromiseIterable = (promises, mapFn) => ), ); const PromiseAll = Promise.all; +const PromiseResolve = Promise.resolve.bind(Promise); const SafePromiseAll = (promises, mapFn) => PromiseAll(arrayToSafePromiseIterable(promises, mapFn)); +const SafePromiseAllReturnArrayLike = (promises, mapFn) => + new Promise((resolve, reject) => { + const { length } = promises; + + const returnVal = Array(length); + ObjectSetPrototypeOf(returnVal, null); + if (length === 0) resolve(returnVal); + + let pendingPromises = length; + for (let i = 0; i < length; i++) { + const promise = mapFn != null ? mapFn(promises[i], i) : promises[i]; + PromisePrototypeThen.$call( + PromiseResolve(promise), + result => { + returnVal[i] = result; + if (--pendingPromises === 0) resolve(returnVal); + }, + reject, + ); + } + }); export default { Array, @@ -113,6 +135,7 @@ export default { }, ), SafePromiseAll, + SafePromiseAllReturnArrayLike, SafeSet: makeSafe( Set, class SafeSet extends Set { diff --git a/src/js/internal/validators.ts b/src/js/internal/validators.ts index e207adfef6..4d91f4563c 100644 --- a/src/js/internal/validators.ts +++ b/src/js/internal/validators.ts @@ -2,6 +2,7 @@ const { hideFromStack } = require("internal/shared"); const RegExpPrototypeExec = RegExp.prototype.exec; const ArrayIsArray = Array.isArray; +const ObjectPrototypeHasOwnProperty = Object.prototype.hasOwnProperty; const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/; /** @@ -63,7 +64,13 @@ function validateLinkHeaderValue(hints) { `must be an array or string of format "; rel=preload; as=style"`, ); } -hideFromStack(validateLinkHeaderValue); + +function validateInternalField(object, fieldKey, className) { + if (typeof object !== "object" || object === null || !ObjectPrototypeHasOwnProperty.$call(object, fieldKey)) { + throw $ERR_INVALID_ARG_TYPE("this", className, object); + } +} +hideFromStack(validateLinkHeaderValue, validateInternalField); export default { /** (value, name) */ @@ -107,4 +114,6 @@ export default { /** `(value, name, oneOf)` */ validateOneOf: $newCppFunction("NodeValidator.cpp", "jsFunction_validateOneOf", 0), isUint8Array: value => value instanceof Uint8Array, + /** `(object, fieldKey, className)` */ + validateInternalField, }; diff --git a/src/js/node/vm.ts b/src/js/node/vm.ts index b277a61432..19103f0969 100644 --- a/src/js/node/vm.ts +++ b/src/js/node/vm.ts @@ -1,14 +1,75 @@ // Hardcoded module "node:vm" +const { SafePromiseAllReturnArrayLike } = require("internal/primordials"); const { throwNotImplemented } = require("internal/shared"); +const { + validateObject, + validateString, + validateUint32, + validateBoolean, + validateInt32, + validateBuffer, + validateFunction, +} = require("internal/validators"); +const util = require("node:util"); const vm = $cpp("NodeVM.cpp", "Bun::createNodeVMBinding"); const ObjectFreeze = Object.freeze; +const ObjectDefineProperty = Object.defineProperty; +const ArrayPrototypeMap = Array.prototype.map; +const PromisePrototypeThen = Promise.prototype.then; +const PromiseResolve = Promise.resolve.bind(Promise); +const ObjectPrototypeHasOwnProperty = Object.prototype.hasOwnProperty; +const ObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; +const ObjectSetPrototypeOf = Object.setPrototypeOf; +const ObjectGetPrototypeOf = Object.getPrototypeOf; +const SymbolToStringTag = Symbol.toStringTag; -const { createContext, isContext, Script, runInNewContext, runInThisContext, compileFunction } = vm; +const kPerContextModuleId = Symbol("kPerContextModuleId"); +const kNative = Symbol("kNative"); +const kContext = Symbol("kContext"); +const kLink = Symbol("kLink"); +const kDependencySpecifiers = Symbol("kDependencySpecifiers"); +const kNoError = Symbol("kNoError"); + +const kEmptyObject = Object.freeze(Object.create(null)); + +const { + Script, + Module: ModuleNative, + createContext, + isContext, + // runInNewContext: moduleRunInNewContext, + // runInThisContext: moduleRunInThisContext, + compileFunction, + isModuleNamespaceObject, + kUnlinked, + kLinked, + kEvaluated, + kErrored, +} = vm; function runInContext(code, context, options) { - return new Script(code, options).runInContext(context); + validateContext(context); + if (typeof options === "string") { + options = { filename: options }; + } + return new Script(code, options).runInContext(context, options); +} + +function runInThisContext(code, options) { + if (typeof options === "string") { + options = { filename: options }; + } + return new Script(code, options).runInThisContext(options); +} + +function runInNewContext(code, contextObject, options) { + if (typeof options === "string") { + options = { filename: options }; + } + contextObject = createContext(contextObject, options); + return createScript(code, options).runInNewContext(contextObject, options); } function createScript(code, options) { @@ -19,15 +80,315 @@ function measureMemory() { throwNotImplemented("node:vm measureMemory"); } -class Module { - constructor() { - throwNotImplemented("node:vm.Module"); +function validateContext(contextifiedObject) { + if (!isContext(contextifiedObject)) { + const error = new Error('The "contextifiedObject" argument must be an vm.Context'); + error.code = "ERR_INVALID_ARG_TYPE"; + error.name = "TypeError"; + throw error; } } -class SourceTextModule { - constructor() { - throwNotImplemented("node:vm.SourceTextModule"); +function validateModule(module, typename = "Module") { + if (!isModule(module)) { + const error = new Error('The "this" argument must be an instance of ' + typename); + error.code = "ERR_INVALID_ARG_TYPE"; + error.name = "TypeError"; + throw error; + } +} + +let globalModuleId = 0; +const defaultModuleName = "vm:module"; + +class Module { + constructor(options) { + if (new.target === Module) { + throw new TypeError("Module is not a constructor"); + } + + const { context, sourceText, syntheticExportNames, syntheticEvaluationSteps } = options; + + if (context !== undefined) { + validateObject(context, "context"); + if (!isContext(context)) { + throw $ERR_INVALID_ARG_TYPE("options.context", "vm.Context", context); + } + } + + let { identifier } = options; + if (identifier !== undefined) { + validateString(identifier, "options.identifier"); + } else if (context === undefined) { + identifier = `${defaultModuleName}(${globalModuleId++})`; + } else if (context[kPerContextModuleId] !== undefined) { + const curId = context[kPerContextModuleId]; + identifier = `${defaultModuleName}(${curId})`; + context[kPerContextModuleId] += 1; + } else { + identifier = `${defaultModuleName}(0)`; + ObjectDefineProperty(context, kPerContextModuleId, { + __proto__: null, + value: 1, + writable: true, + enumerable: false, + configurable: true, + }); + } + + let registry = { __proto__: null }; + if (sourceText !== undefined) { + this[kNative] = new ModuleNative( + identifier, + context, + sourceText, + options.lineOffset, + options.columnOffset, + options.cachedData, + ); + registry = { + __proto__: null, + initializeImportMeta: options.initializeImportMeta, + importModuleDynamically: options.importModuleDynamically + ? importModuleDynamicallyWrap(options.importModuleDynamically) + : undefined, + }; + // This will take precedence over the referrer as the object being + // passed into the callbacks. + registry.callbackReferrer = this; + // const { registerModule } = require("internal/modules/esm/utils"); + // registerModule(this[kNative], registry); + } else { + $assert(syntheticEvaluationSteps); + this[kNative] = new ModuleNative(identifier, context, syntheticExportNames, syntheticEvaluationSteps); + } + + this[kContext] = context; + } + + get identifier() { + validateModule(this); + return this[kNative].identifier; + } + + get context() { + validateModule(this); + return this[kContext]; + } + + get status() { + validateModule(this); + return this[kNative].getStatus(); + } + + get namespace() { + validateModule(this); + if (this[kNative].getStatusCode() < kLinked) { + throw $ERR_VM_MODULE_STATUS("must not be unlinked or linking"); + } + + return this[kNative].getNamespace(); + } + + get error() { + validateModule(this); + if (this[kNative].getStatusCode() !== kErrored) { + throw $ERR_VM_MODULE_STATUS("must be errored"); + } + + return this[kNative].getError(); + } + + async link(linker) { + validateModule(this); + validateFunction(linker, "linker"); + + if (this[kNative].getStatusCode() === kLinked) { + throw $ERR_VM_MODULE_ALREADY_LINKED(); + } + + if (this[kNative].getStatusCode() !== kUnlinked) { + throw $ERR_VM_MODULE_STATUS("must be unlinked"); + } + + await this[kLink](linker); + this[kNative].instantiate(); + } + + async evaluate(options = kEmptyObject) { + validateModule(this); + validateObject(options, "options"); + + let timeout = options.timeout; + if (timeout === undefined) { + timeout = -1; + } else { + validateUint32(timeout, "options.timeout", true); + } + const { breakOnSigint = false } = options; + validateBoolean(breakOnSigint, "options.breakOnSigint"); + const status = this[kNative].getStatusCode(); + if (status !== kLinked && status !== kEvaluated && status !== kErrored) { + throw $ERR_VM_MODULE_STATUS("must be one of linked, evaluated, or errored"); + } + await this[kNative].evaluate(timeout, breakOnSigint); + } + + [util.inspect.custom](depth, options) { + validateModule(this); + if (typeof depth === "number" && depth < 0) return this; + + const constructor = getConstructorOf(this) || Module; + const o = { __proto__: { constructor } }; + o.status = this.status; + o.identifier = this.identifier; + o.context = this.context; + + ObjectSetPrototypeOf(o, ObjectGetPrototypeOf(this)); + ObjectDefineProperty(o, SymbolToStringTag, { + __proto__: null, + value: constructor.name, + configurable: true, + }); + + return util.inspect(o, { ...options, customInspect: false }); + } +} + +class SourceTextModule extends Module { + #error: any = kNoError; + #statusOverride: any; + + constructor(sourceText, options = kEmptyObject) { + validateString(sourceText, "sourceText"); + validateObject(options, "options"); + + const { + lineOffset = 0, + columnOffset = 0, + initializeImportMeta, + importModuleDynamically, + context, + identifier, + cachedData, + } = options; + + validateInt32(lineOffset, "options.lineOffset"); + validateInt32(columnOffset, "options.columnOffset"); + + if (initializeImportMeta !== undefined) { + validateFunction(initializeImportMeta, "options.initializeImportMeta"); + } + + if (importModuleDynamically !== undefined) { + validateFunction(importModuleDynamically, "options.importModuleDynamically"); + } + + if (cachedData !== undefined) { + validateBuffer(cachedData, "options.cachedData"); + } + + super({ + sourceText, + context, + identifier, + lineOffset, + columnOffset, + cachedData, + initializeImportMeta, + importModuleDynamically, + }); + + this[kDependencySpecifiers] = undefined; + } + + async [kLink](linker) { + validateModule(this, "SourceTextModule"); + + if (this[kNative].getStatusCode() >= kLinked) { + throw $ERR_VM_MODULE_ALREADY_LINKED(); + } + + this.#statusOverride = "linking"; + const moduleRequests = this[kNative].createModuleRecord(); + + // Iterates the module requests and links with the linker. + // Specifiers should be aligned with the moduleRequests array in order. + const specifiers = Array(moduleRequests.length); + const modulePromises = Array(moduleRequests.length); + // Iterates with index to avoid calling into userspace with `Symbol.iterator`. + for (let idx = 0; idx < moduleRequests.length; idx++) { + const { specifier, attributes } = moduleRequests[idx]; + + const linkerResult = linker(specifier, this, { + attributes, + assert: attributes, + }); + + const modulePromise = PromisePrototypeThen.$call(PromiseResolve(linkerResult), async mod => { + if (!isModule(mod)) { + throw $ERR_VM_MODULE_NOT_MODULE(); + } + if (mod.context !== this.context) { + throw $ERR_VM_MODULE_DIFFERENT_CONTEXT(); + } + if (mod.status === "errored") { + throw $ERR_VM_MODULE_LINK_FAILURE(`request for '${specifier}' resolved to an errored mod`, mod.error); + } + if (mod.status === "unlinked") { + await mod[kLink](linker); + } + return mod[kNative]; + }); + modulePromises[idx] = modulePromise; + specifiers[idx] = specifier; + } + + try { + const moduleNatives = await SafePromiseAllReturnArrayLike(modulePromises); + this[kNative].link(specifiers, moduleNatives, 0); + } catch (e) { + this.#error = e; + throw e; + } finally { + this.#statusOverride = undefined; + } + } + + get dependencySpecifiers() { + validateModule(this, "SourceTextModule"); + this[kDependencySpecifiers] ??= ObjectFreeze( + ArrayPrototypeMap.$call(this[kNative].getModuleRequests(), request => request[0]), + ); + return this[kDependencySpecifiers]; + } + + get status() { + validateModule(this, "SourceTextModule"); + if (this.#error !== kNoError) { + return "errored"; + } + if (this.#statusOverride) { + return this.#statusOverride; + } + return super.status; + } + + get error() { + validateModule(this, "SourceTextModule"); + if (this.#error !== kNoError) { + return this.#error; + } + return super.error; + } + + createCachedData() { + validateModule(this, "SourceTextModule"); + const { status } = this; + if (status === "evaluating" || status === "evaluated" || status === "errored") { + throw $ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA(); + } + return this[kNative].createCachedData(); } } @@ -43,6 +404,38 @@ const constants = { DONT_CONTEXTIFY: Symbol("vm_context_no_contextify"), }; +function isModule(object) { + return typeof object === "object" && object !== null && ObjectPrototypeHasOwnProperty.$call(object, kNative); +} + +function importModuleDynamicallyWrap(importModuleDynamically) { + const importModuleDynamicallyWrapper = async (...args) => { + const m: any = importModuleDynamically.$apply(this, args); + if (isModuleNamespaceObject(m)) { + return m; + } + if (!isModule(m)) { + throw $ERR_VM_MODULE_NOT_MODULE(); + } + if (m.status === "errored") { + throw m.error; + } + return m.namespace; + }; + return importModuleDynamicallyWrapper; +} + +function getConstructorOf(obj) { + while (obj) { + const descriptor = ObjectGetOwnPropertyDescriptor(obj, "constructor"); + if (descriptor !== undefined && typeof descriptor.value === "function" && descriptor.value.name !== "") { + return descriptor.value; + } + + obj = ObjectGetPrototypeOf(obj); + } +} + export default { createContext, runInContext, diff --git a/src/vm/Semaphore.cpp b/src/vm/Semaphore.cpp new file mode 100644 index 0000000000..f1eec7ef05 --- /dev/null +++ b/src/vm/Semaphore.cpp @@ -0,0 +1,51 @@ +#include "Semaphore.h" + +namespace Bun { + +Semaphore::Semaphore(unsigned int value) +{ +#if OS(WINDOWS) + uv_sem_init(&m_semaphore, value); +#elif OS(DARWIN) + semaphore_create(mach_task_self(), &m_semaphore, SYNC_POLICY_FIFO, value); +#else + sem_init(&m_semaphore, 0, value); +#endif +} + +Semaphore::~Semaphore() +{ +#if OS(WINDOWS) + uv_sem_destroy(&m_semaphore); +#elif OS(DARWIN) + semaphore_destroy(mach_task_self(), m_semaphore); +#else + sem_destroy(&m_semaphore); +#endif +} + +bool Semaphore::signal() +{ +#if OS(WINDOWS) + uv_sem_post(&m_semaphore); + return true; +#elif OS(DARWIN) + return semaphore_signal(m_semaphore) == KERN_SUCCESS; +#else + return sem_post(&m_semaphore) == 0; +#endif +} + +bool Semaphore::wait() +{ +#if OS(WINDOWS) + uv_sem_wait(&m_semaphore); + return true; +#elif OS(DARWIN) + return semaphore_wait(m_semaphore) == KERN_SUCCESS; +#else + return sem_wait(&m_semaphore) == 0; +#endif +} + +} // namespace Bun diff --git a/src/vm/Semaphore.h b/src/vm/Semaphore.h new file mode 100644 index 0000000000..cb3ae7d93c --- /dev/null +++ b/src/vm/Semaphore.h @@ -0,0 +1,34 @@ +#pragma once + +#include "root.h" + +#if OS(WINDOWS) +#include +#elif OS(DARWIN) +#include +#include +#else +#include +#endif + +namespace Bun { + +class Semaphore { +public: + Semaphore(unsigned int value); + ~Semaphore(); + + bool signal(); + bool wait(); + +private: +#if OS(WINDOWS) + uv_sem_t m_semaphore; +#elif OS(DARWIN) + semaphore_t m_semaphore; +#else + sem_t m_semaphore; +#endif +}; + +} // namespace Bun diff --git a/src/vm/SigintReceiver.h b/src/vm/SigintReceiver.h new file mode 100644 index 0000000000..fc66d43b3c --- /dev/null +++ b/src/vm/SigintReceiver.h @@ -0,0 +1,23 @@ +#pragma once + +namespace Bun { + +class SigintReceiver { +public: + SigintReceiver() = default; + + void setSigintReceived(bool value = true) + { + m_sigintReceived = value; + } + + bool getSigintReceived() + { + return m_sigintReceived; + } + +protected: + bool m_sigintReceived = false; +}; + +} // namespace Bun diff --git a/src/vm/SigintWatcher.cpp b/src/vm/SigintWatcher.cpp new file mode 100644 index 0000000000..8956fe29db --- /dev/null +++ b/src/vm/SigintWatcher.cpp @@ -0,0 +1,208 @@ +#include "NodeVM.h" +#include "SigintWatcher.h" + +#if OS(WINDOWS) +#include +#endif + +extern "C" void Bun__onPosixSignal(int signalNumber); +extern "C" void Bun__ensureSignalHandler(); + +namespace Bun { + +#if OS(WINDOWS) +static BOOL WindowsCtrlHandler(DWORD signal) +{ + if (signal == CTRL_C_EVENT) { + SigintWatcher::get().signalReceived(); + return true; + } + + return false; +} +#endif + +SigintWatcher::SigintWatcher() + : m_semaphore(1) +{ + m_globalObjects.reserveInitialCapacity(16); +} + +SigintWatcher::~SigintWatcher() +{ + uninstall(); +} + +void SigintWatcher::install() +{ +#if OS(WINDOWS) + SetConsoleCtrlHandler(WindowsCtrlHandler, true); +#else + Bun__ensureSignalHandler(); + + struct sigaction action; + memset(&action, 0, sizeof(struct sigaction)); + + action.sa_handler = [](int signalNumber) { + get().signalReceived(); + }; + + sigemptyset(&action.sa_mask); + sigaddset(&action.sa_mask, SIGINT); + action.sa_flags = 0; + + sigaction(SIGINT, &action, nullptr); +#endif + + if (m_installed.exchange(true)) { + return; + } + + m_thread = WTF::Thread::create("SigintWatcher"_s, [this] { + while (m_installed.load()) { + bool success = m_semaphore.wait(); + if (!m_installed) { + return; + } + ASSERT(success); + if (m_waiting.test_and_set()) { + m_waiting.clear(); +#if !OS(WINDOWS) + if (!signalAll()) { + Bun__onPosixSignal(SIGINT); + } +#else + signalAll(); +#endif + } else { + m_waiting.clear(); + } + } + }); +} + +void SigintWatcher::uninstall() +{ + if (m_installed.exchange(false)) { + WTF::Thread* currentThread = WTF::Thread::currentMayBeNull(); + ASSERT(!currentThread || m_thread->uid() != currentThread->uid()); + +#if OS(WINDOWS) + SetConsoleCtrlHandler(WindowsCtrlHandler, false); +#else + struct sigaction action; + memset(&action, 0, sizeof(struct sigaction)); + action.sa_handler = SIG_DFL; + sigemptyset(&action.sa_mask); + sigaddset(&action.sa_mask, SIGINT); + action.sa_flags = SA_RESTART; + sigaction(SIGINT, &action, nullptr); +#endif + + m_semaphore.signal(); + m_thread->waitForCompletion(); + } +} + +void SigintWatcher::signalReceived() +{ + if (!m_waiting.test_and_set()) { + bool success = m_semaphore.signal(); + ASSERT(success); + } +} + +void SigintWatcher::registerGlobalObject(JSGlobalObject* globalObject) +{ + if (globalObject == nullptr) { + return; + } + + WTF::Locker lock(m_globalObjectsMutex); + m_globalObjects.appendIfNotContains(globalObject); +} + +void SigintWatcher::unregisterGlobalObject(JSGlobalObject* globalObject) +{ + if (globalObject == nullptr) { + return; + } + + WTF::Locker lock(m_globalObjectsMutex); + + auto iter = std::find(m_globalObjects.begin(), m_globalObjects.end(), globalObject); + if (iter == m_globalObjects.end()) { + return; + } + + std::swap(*iter, m_globalObjects.last()); + m_globalObjects.removeLast(); +} + +void SigintWatcher::registerReceiver(SigintReceiver* module) +{ + if (module == nullptr) { + return; + } + + WTF::Locker lock(m_receiversMutex); + m_receivers.appendIfNotContains(module); +} + +void SigintWatcher::unregisterReceiver(SigintReceiver* module) +{ + WTF::Locker lock(m_receiversMutex); + + auto iter = std::find(m_receivers.begin(), m_receivers.end(), module); + if (iter == m_receivers.end()) { + return; + } + + std::swap(*iter, m_receivers.last()); + m_receivers.removeLast(); +} + +void SigintWatcher::ref() +{ + if (m_refCount++ == 0) { + install(); + } +} + +void SigintWatcher::deref() +{ + ASSERT(m_refCount > 0); + if (--m_refCount == 0) { + uninstall(); + } +} + +SigintWatcher& SigintWatcher::get() +{ + static SigintWatcher instance; + return instance; +} + +bool SigintWatcher::signalAll() +{ + { + WTF::Locker lock(m_receiversMutex); + for (auto* receiver : m_receivers) { + receiver->setSigintReceived(); + } + } + + WTF::Locker lock(m_globalObjectsMutex); + + if (m_globalObjects.isEmpty()) { + return false; + } + + for (JSGlobalObject* globalObject : m_globalObjects) { + globalObject->vm().notifyNeedTermination(); + } + + return true; +} + +} // namespace Bun diff --git a/src/vm/SigintWatcher.h b/src/vm/SigintWatcher.h new file mode 100644 index 0000000000..dbdf33c1c4 --- /dev/null +++ b/src/vm/SigintWatcher.h @@ -0,0 +1,110 @@ +#pragma once + +#include "root.h" + +#include "Semaphore.h" +#include "SigintReceiver.h" + +#include + +namespace Bun { + +template +concept SigintHoldable = std::derived_from || std::derived_from; + +class SigintWatcher { +public: + SigintWatcher(); + ~SigintWatcher(); + + void install(); + void uninstall(); + void signalReceived(); + void registerGlobalObject(JSC::JSGlobalObject* globalObject); + void unregisterGlobalObject(JSC::JSGlobalObject* globalObject); + void registerReceiver(SigintReceiver* module); + void unregisterReceiver(SigintReceiver* module); + /** Installs the signal handler if it's not already installed and increments the ref count. */ + void ref(); + /** Decrements the ref count and uninstalls the signal handler if the ref count reaches 0. */ + void deref(); + + static SigintWatcher& get(); + + class GlobalObjectHolder { + public: + template + ALWAYS_INLINE GlobalObjectHolder(Ts*... held) + { + (assign(held), ...); + } + + ~GlobalObjectHolder() + { + for (auto* receiver : m_receivers) { + get().unregisterReceiver(receiver); + } + + if (m_globalObject) { + get().unregisterGlobalObject(m_globalObject); + get().deref(); + } + } + + GlobalObjectHolder(const GlobalObjectHolder&) = delete; + GlobalObjectHolder(GlobalObjectHolder&& other) + : m_globalObject(std::exchange(other.m_globalObject, nullptr)) + , m_receivers(WTFMove(other.m_receivers)) + { + } + + GlobalObjectHolder& operator=(const GlobalObjectHolder&) = delete; + GlobalObjectHolder& operator=(GlobalObjectHolder&& other) + { + m_globalObject = std::exchange(other.m_globalObject, nullptr); + m_receivers = WTFMove(other.m_receivers); + return *this; + } + + void ALWAYS_INLINE assign(SigintHoldable auto* ptr) + { + using T = std::remove_pointer_t; + if constexpr (std::derived_from) { + if ((m_globalObject = ptr)) { + get().ref(); + get().registerGlobalObject(m_globalObject); + } + } else if constexpr (std::derived_from) { + m_receivers.append(ptr); + get().registerReceiver(ptr); + } else { + static_assert(false, "Invalid held type"); + } + } + + private: + JSC::JSGlobalObject* m_globalObject = nullptr; + WTF::Vector m_receivers; + }; + + template + ALWAYS_INLINE static GlobalObjectHolder hold(Ts*... held) + { + return { held... }; + } + +private: + RefPtr m_thread; + std::atomic_bool m_installed = false; + std::atomic_flag m_waiting {}; + Semaphore m_semaphore; + WTF::Lock m_globalObjectsMutex; + WTF::Lock m_receiversMutex; + WTF::Vector m_globalObjects; + WTF::Vector m_receivers; + uint32_t m_refCount = 0; + + bool signalAll(); +}; + +} // namespace Bun diff --git a/test/js/node/test/parallel/test-vm-basic.js b/test/js/node/test/parallel/test-vm-basic.js new file mode 100644 index 0000000000..f743d53170 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-basic.js @@ -0,0 +1,350 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +// vm.runInNewContext +{ + const sandbox = {}; + const result = vm.runInNewContext( + 'foo = "bar"; this.typeofProcess = typeof process; typeof Object;', + sandbox + ); + assert.deepStrictEqual(sandbox, { + foo: 'bar', + typeofProcess: 'undefined', + }); + assert.strictEqual(result, 'function'); +} + +// vm.runInContext +{ + const sandbox = { foo: 'bar' }; + const context = vm.createContext(sandbox); + const result = vm.runInContext( + 'baz = foo; this.typeofProcess = typeof process; typeof Object;', + context + ); + assert.deepStrictEqual(sandbox, { + foo: 'bar', + baz: 'bar', + typeofProcess: 'undefined' + }); + assert.strictEqual(result, 'function'); +} + +// vm.runInThisContext +{ + const result = vm.runInThisContext( + 'vmResult = "foo"; Object.prototype.toString.call(process);' + ); + assert.strictEqual(global.vmResult, 'foo'); + assert.strictEqual(result, '[object process]'); + delete global.vmResult; +} + +// vm.runInNewContext +{ + const result = vm.runInNewContext( + 'vmResult = "foo"; typeof process;' + ); + assert.strictEqual(global.vmResult, undefined); + assert.strictEqual(result, 'undefined'); +} + +// vm.createContext +{ + const sandbox = {}; + const context = vm.createContext(sandbox); + assert.strictEqual(sandbox, context); +} + +// Run script with filename +{ + const script = 'throw new Error("boom")'; + const filename = 'test-boom-error'; + const context = vm.createContext(); + + function checkErr(err) { + return err.stack.startsWith('test-boom-error:1'); + } + + assert.throws(() => vm.runInContext(script, context, filename), checkErr); + assert.throws(() => vm.runInNewContext(script, context, filename), checkErr); + assert.throws(() => vm.runInThisContext(script, filename), checkErr); +} + +// Invalid arguments +[null, 'string'].forEach((input) => { + assert.throws(() => { + vm.createContext({}, input); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options" argument must be of type object.' + + common.invalidArgTypeHelper(input) + }); +}); + +['name', 'origin'].forEach((propertyName) => { + assert.throws(() => { + vm.createContext({}, { [propertyName]: null }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: `The "options.${propertyName}" property must be of type string. ` + + 'Received null' + }); +}); + +['contextName', 'contextOrigin'].forEach((propertyName) => { + assert.throws(() => { + vm.runInNewContext('', {}, { [propertyName]: null }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: `The "options.${propertyName}" property must be of type string. ` + + 'Received null' + }); +}); + +// vm.compileFunction +{ + assert.strictEqual( + vm.compileFunction('console.log("Hello, World!")').toString(), + 'function () {\nconsole.log("Hello, World!")\n}' + ); + + assert.strictEqual( + vm.compileFunction( + 'return p + q + r + s + t', + ['p', 'q', 'r', 's', 't'] + )('ab', 'cd', 'ef', 'gh', 'ij'), + 'abcdefghij' + ); + + vm.compileFunction('return'); // Should not throw on 'return' + + assert.throws(() => { + vm.compileFunction( + '});\n\n(function() {\nconsole.log(1);\n})();\n\n(function() {' + ); + }, { + name: 'SyntaxError', + message: "Unexpected token '}'" + }); + + // Tests for failed argument validation + assert.throws(() => vm.compileFunction(), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "code" argument must be of type string. ' + + 'Received undefined' + }); + + vm.compileFunction(''); // Should pass without params or options + + assert.throws(() => vm.compileFunction('', null), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "params" argument must be an instance of Array. ' + + 'Received null' + }); + + // vm.compileFunction('', undefined, null); + + const optionTypes = { + 'filename': 'string', + 'columnOffset': 'number', + 'lineOffset': 'number', + 'cachedData': 'Buffer, TypedArray, or DataView', + 'produceCachedData': 'boolean', + }; + + for (const option in optionTypes) { + const typeErrorMessage = `The "options.${option}" property must be ` + + (option === 'cachedData' ? 'an instance of' : 'of type'); + assert.throws(() => { + vm.compileFunction('', undefined, { [option]: null }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: typeErrorMessage + + ` ${optionTypes[option]}. Received null` + }); + } + + // Testing for context-based failures + [Boolean(), Number(), null, String(), Symbol(), {}].forEach( + (value) => { + assert.throws(() => { + vm.compileFunction('', undefined, { parsingContext: value }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options.parsingContext" property must be an instance ' + + `of Context.${common.invalidArgTypeHelper(value)}` + }); + } + ); + + // Testing for non Array type-based failures + [Boolean(), Number(), null, Object(), Symbol(), {}].forEach( + (value) => { + assert.throws(() => { + vm.compileFunction('', value); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "params" argument must be an instance of Array.' + + common.invalidArgTypeHelper(value) + }); + } + ); + + assert.strictEqual( + vm.compileFunction( + 'return a;', + undefined, + { contextExtensions: [{ a: 5 }] } + )(), + 5 + ); + + assert.throws(() => { + vm.compileFunction('', undefined, { contextExtensions: null }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options.contextExtensions" property must be an instance of' + + ' Array. Received null' + }); + + assert.throws(() => { + vm.compileFunction('', undefined, { contextExtensions: [0] }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options.contextExtensions[0]" property must be of type ' + + 'object. Received type number (0)' + }); + + const oldLimit = Error.stackTraceLimit; + // Setting value to run the last three tests + Error.stackTraceLimit = 1; + + assert.throws(() => { + vm.compileFunction('throw new Error("Sample Error")')(); + }, { + message: 'Sample Error', + stack: 'Error: Sample Error\n at (file:///:2:16)' // from :1:7 to match Bun's stack trace formatting + }); + + assert.throws(() => { + vm.compileFunction( + 'throw new Error("Sample Error")', + [], + { lineOffset: 3 } + )(); + }, { + message: 'Sample Error', + stack: 'Error: Sample Error\n at (evalmachine.:5:16)' // modified from :4:7 to match Bun's stack trace formatting + }); + + assert.throws(() => { + vm.compileFunction( + 'throw new Error("Sample Error")', + [], + { columnOffset: 3 } + )(); + }, { + message: 'Sample Error', + stack: 'Error: Sample Error\n at (evalmachine.:2:16)' // modified from :1:10 to match Bun's stack trace formatting + }); + + assert.strictEqual( + vm.compileFunction( + 'return varInContext', + [], + { + parsingContext: vm.createContext({ varInContext: 'abc' }) + } + )(), + 'abc' + ); + + assert.throws(() => { + vm.compileFunction( + 'return varInContext', + [] + )(); + }, { + message: 'varInContext is not defined', + stack: 'ReferenceError: varInContext is not defined\n at (file:///:2:20)' // modified from :1:1 to match Bun's stack trace formatting + }); + + assert.notDeepStrictEqual( + vm.compileFunction( + 'return global', + [], + { + parsingContext: vm.createContext({ global: {} }) + } + )(), + global + ); + + assert.deepStrictEqual( + vm.compileFunction( + 'return global', + [] + )(), + global + ); + + { + const source = 'console.log("Hello, World!")'; + // Test compileFunction produceCachedData option + const result = vm.compileFunction(source, [], { + produceCachedData: true, + }); + + assert.ok(result.cachedDataProduced); + assert.ok(result.cachedData.length > 0); + + // Test compileFunction cachedData consumption + const result2 = vm.compileFunction(source, [], { + cachedData: result.cachedData + }); + assert.strictEqual(result2.cachedDataRejected, false); + + const result3 = vm.compileFunction('console.log("wrong source")', [], { + cachedData: result.cachedData + }); + assert.strictEqual(result3.cachedDataRejected, true); + } + + // Resetting value + Error.stackTraceLimit = oldLimit; +} diff --git a/test/js/node/test/parallel/test-vm-module-cached-data.js b/test/js/node/test/parallel/test-vm-module-cached-data.js new file mode 100644 index 0000000000..fef68a8c18 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-module-cached-data.js @@ -0,0 +1,30 @@ +'use strict'; + +// Flags: --experimental-vm-modules + +const common = require('../common'); + +const assert = require('assert'); +const { SourceTextModule } = require('vm'); + +{ + const m = new SourceTextModule('const a = 1'); + const cachedData = m.createCachedData(); + + new SourceTextModule('const a = 1', { cachedData }); + + assert.throws(() => { + new SourceTextModule('differentSource', { cachedData }); + }, { + code: 'ERR_VM_MODULE_CACHED_DATA_REJECTED', + }); +} + +assert.rejects(async () => { + const m = new SourceTextModule('const a = 1'); + await m.link(() => {}); + m.evaluate(); + m.createCachedData(); +}, { + code: 'ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA', +}).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-vm-module-errors.js b/test/js/node/test/parallel/test-vm-module-errors.js new file mode 100644 index 0000000000..61f9c68fa3 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-module-errors.js @@ -0,0 +1,276 @@ +'use strict'; + +// Flags: --experimental-vm-modules + +const common = require('../common'); + +const assert = require('assert'); + +const { SourceTextModule, createContext, Module } = require('vm'); + +async function createEmptyLinkedModule() { + const m = new SourceTextModule(''); + await m.link(common.mustNotCall()); + return m; +} + +async function checkArgType() { + assert.throws(() => { + new SourceTextModule(); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); + + for (const invalidOptions of [ + 0, 1, null, true, 'str', () => {}, { identifier: 0 }, Symbol.iterator, + { context: null }, { context: 'hucairz' }, { context: {} }, + ]) { + assert.throws(() => { + new SourceTextModule('', invalidOptions); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); + } + + for (const invalidLinker of [ + 0, 1, undefined, null, true, 'str', {}, Symbol.iterator, + ]) { + await assert.rejects(async () => { + const m = new SourceTextModule(''); + await m.link(invalidLinker); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); + } +} + +// Check methods/properties can only be used under a specific state. +async function checkModuleState() { + await assert.rejects(async () => { + const m = new SourceTextModule(''); + await m.link(common.mustNotCall()); + assert.strictEqual(m.status, 'linked'); + await m.link(common.mustNotCall()); + }, { + code: 'ERR_VM_MODULE_ALREADY_LINKED' + }); + + await assert.rejects(async () => { + const m = new SourceTextModule(''); + m.link(common.mustNotCall()); + assert.strictEqual(m.status, 'linking'); + await m.link(common.mustNotCall()); + }, { + code: 'ERR_VM_MODULE_STATUS' + }); + + await assert.rejects(async () => { + const m = new SourceTextModule(''); + await m.evaluate(); + }, { + code: 'ERR_VM_MODULE_STATUS', + message: 'Module status must be one of linked, evaluated, or errored' + }); + + await assert.rejects(async () => { + const m = new SourceTextModule(''); + await m.evaluate(false); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options" argument must be of type object. ' + + 'Received type boolean (false)' + }); + + assert.throws(() => { + const m = new SourceTextModule(''); + m.error; // eslint-disable-line no-unused-expressions + }, { + code: 'ERR_VM_MODULE_STATUS', + message: 'Module status must be errored' + }); + + await assert.rejects(async () => { + const m = await createEmptyLinkedModule(); + await m.evaluate(); + m.error; // eslint-disable-line no-unused-expressions + }, { + code: 'ERR_VM_MODULE_STATUS', + message: 'Module status must be errored' + }); + + assert.throws(() => { + const m = new SourceTextModule(''); + m.namespace; // eslint-disable-line no-unused-expressions + }, { + code: 'ERR_VM_MODULE_STATUS', + message: 'Module status must not be unlinked or linking' + }); +} + +// Check link() fails when the returned module is not valid. +async function checkLinking() { + await assert.rejects(async () => { + const m = new SourceTextModule('import "foo";'); + try { + await m.link(common.mustCall(() => ({}))); + } catch (err) { + assert.strictEqual(m.status, 'errored'); + throw err; + } + }, { + code: 'ERR_VM_MODULE_NOT_MODULE' + }); + + await assert.rejects(async () => { + const c = createContext({ a: 1 }); + const foo = new SourceTextModule('', { context: c }); + await foo.link(common.mustNotCall()); + const bar = new SourceTextModule('import "foo";'); + try { + await bar.link(common.mustCall(() => foo)); + } catch (err) { + assert.strictEqual(bar.status, 'errored'); + throw err; + } + }, { + code: 'ERR_VM_MODULE_DIFFERENT_CONTEXT' + }); + + const error = new Error(); + await assert.rejects(async () => { + globalThis.error = error; + const erroredModule = new SourceTextModule('throw error;'); + await erroredModule.link(common.mustNotCall()); + try { + await erroredModule.evaluate(); + } catch { + // ignored + } + delete globalThis.error; + + assert.strictEqual(erroredModule.status, 'errored'); + + const rootModule = new SourceTextModule('import "errored";'); + await rootModule.link(common.mustCall(() => erroredModule)); + }, { + code: 'ERR_VM_MODULE_LINK_FAILURE', + cause: error, + }); +} + +assert.throws(() => { + new SourceTextModule('', { + importModuleDynamically: 'hucairz' + }); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options.importModuleDynamically" property must be of type ' + + "function. Received type string ('hucairz')" +}); + +// Check the JavaScript engine deals with exceptions correctly +async function checkExecution() { + await (async () => { + const m = new SourceTextModule('import { nonexistent } from "module";'); + + // There is no code for this exception since it is thrown by the JavaScript + // engine. + await assert.rejects(() => { + return m.link(common.mustCall(() => new SourceTextModule(''))); + }, SyntaxError); + })(); + + await (async () => { + const m = new SourceTextModule('throw new Error();'); + await m.link(common.mustNotCall()); + try { + await m.evaluate(); + } catch (err) { + assert.strictEqual(m.error, err); + assert.strictEqual(m.status, 'errored'); + return; + } + assert.fail('Missing expected exception'); + })(); +} + +// Check for error thrown when breakOnSigint is not a boolean for evaluate() +async function checkInvalidOptionForEvaluate() { + await assert.rejects(async () => { + const m = new SourceTextModule('export const a = 1; export let b = 2'); + await m.evaluate({ breakOnSigint: 'a-string' }); + }, { + name: 'TypeError', + message: + 'The "options.breakOnSigint" property must be of type boolean. ' + + "Received type string ('a-string')", + code: 'ERR_INVALID_ARG_TYPE' + }); + + { + ['link', 'evaluate'].forEach(async (method) => { + await assert.rejects(async () => { + await Module.prototype[method](); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "this" argument must be an instance of Module/ + }); + }); + } +} + +function checkInvalidCachedData() { + [true, false, 'foo', {}, Array, function() {}].forEach((invalidArg) => { + const message = 'The "options.cachedData" property must be of ' + + 'type Buffer, TypedArray, or DataView.' + + common.invalidArgTypeHelper(invalidArg); + assert.throws( + () => new SourceTextModule('import "foo";', { cachedData: invalidArg }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message, + } + ); + }); +} + +function checkGettersErrors() { + const expectedError = { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "this" argument must be an instance of (?:Module|SourceTextModule)/, + }; + const getters = ['identifier', 'context', 'namespace', 'status', 'error']; + getters.forEach((getter) => { + assert.throws(() => { + // eslint-disable-next-line no-unused-expressions + Module.prototype[getter]; + }, expectedError); + assert.throws(() => { + // eslint-disable-next-line no-unused-expressions + SourceTextModule.prototype[getter]; + }, expectedError); + }); + // `dependencySpecifiers` getter is just part of SourceTextModule + assert.throws(() => { + // eslint-disable-next-line no-unused-expressions + SourceTextModule.prototype.dependencySpecifiers; + }, expectedError); +} + +const finished = common.mustCall(); + +(async function main() { + await checkArgType(); + await checkModuleState(); + await checkLinking(); + await checkExecution(); + await checkInvalidOptionForEvaluate(); + checkInvalidCachedData(); + checkGettersErrors(); + finished(); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-vm-module-link.js b/test/js/node/test/parallel/test-vm-module-link.js new file mode 100644 index 0000000000..26dcb69885 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-module-link.js @@ -0,0 +1,168 @@ +'use strict'; + +// Flags: --experimental-vm-modules --harmony-import-attributes + +const common = require('../common'); + +const assert = require('assert'); + +const { SourceTextModule } = require('vm'); + +async function simple() { + const foo = new SourceTextModule('export default 5;'); + await foo.link(common.mustNotCall()); + + globalThis.fiveResult = undefined; + const bar = new SourceTextModule('import five from "foo"; fiveResult = five'); + + assert.deepStrictEqual(bar.dependencySpecifiers, ['foo']); + + await bar.link(common.mustCall((specifier, module) => { + assert.strictEqual(module, bar); + assert.strictEqual(specifier, 'foo'); + return foo; + })); + + await bar.evaluate(); + assert.strictEqual(globalThis.fiveResult, 5); + delete globalThis.fiveResult; +} + +async function invalidLinkValue() { + const invalidValues = [ + undefined, + null, + {}, + SourceTextModule.prototype, + ]; + + for (const value of invalidValues) { + const module = new SourceTextModule('import "foo"'); + await assert.rejects(module.link(() => value), { + code: 'ERR_VM_MODULE_NOT_MODULE', + }); + } +} + +async function depth() { + const foo = new SourceTextModule('export default 5'); + await foo.link(common.mustNotCall()); + + async function getProxy(parentName, parentModule) { + const mod = new SourceTextModule(` + import ${parentName} from '${parentName}'; + export default ${parentName}; + `); + await mod.link(common.mustCall((specifier, module) => { + assert.strictEqual(module, mod); + assert.strictEqual(specifier, parentName); + return parentModule; + })); + return mod; + } + + const bar = await getProxy('foo', foo); + const baz = await getProxy('bar', bar); + const barz = await getProxy('baz', baz); + + await barz.evaluate(); + + assert.strictEqual(barz.namespace.default, 5); +} + +async function circular() { + const foo = new SourceTextModule(` + import getFoo from 'bar'; + export let foo = 42; + export default getFoo(); + `); + const bar = new SourceTextModule(` + import { foo } from 'foo'; + export default function getFoo() { + return foo; + } + `); + await foo.link(common.mustCall(async (specifier, module) => { + if (specifier === 'bar') { + assert.strictEqual(module, foo); + return bar; + } + assert.strictEqual(specifier, 'foo'); + assert.strictEqual(module, bar); + assert.strictEqual(foo.status, 'linking'); + return foo; + }, 2)); + + assert.strictEqual(bar.status, 'linked'); + + await foo.evaluate(); + assert.strictEqual(foo.namespace.default, 42); +} + +async function circular2() { + const sourceMap = { + 'root': ` + import * as a from './a.mjs'; + import * as b from './b.mjs'; + if (!('fromA' in a)) + throw new Error(); + if (!('fromB' in a)) + throw new Error(); + if (!('fromA' in b)) + throw new Error(); + if (!('fromB' in b)) + throw new Error(); + `, + './a.mjs': ` + export * from './b.mjs'; + export let fromA; + `, + './b.mjs': ` + export * from './a.mjs'; + export let fromB; + ` + }; + const moduleMap = new Map(); + const rootModule = new SourceTextModule(sourceMap.root, { + identifier: 'vm:root', + }); + async function link(specifier, referencingModule) { + if (moduleMap.has(specifier)) { + return moduleMap.get(specifier); + } + const mod = new SourceTextModule(sourceMap[specifier], { + identifier: new URL(specifier, 'file:///').href, + }); + moduleMap.set(specifier, mod); + return mod; + } + await rootModule.link(link); + await rootModule.evaluate(); +} + +async function asserts() { + const m = new SourceTextModule(` + import "foo" with { n1: 'v1', n2: 'v2' }; + `, { identifier: 'm' }); + await m.link((s, r, p) => { + assert.strictEqual(s, 'foo'); + assert.strictEqual(r.identifier, 'm'); + assert.strictEqual(p.attributes.n1, 'v1'); + assert.strictEqual(p.assert.n1, 'v1'); + assert.strictEqual(p.attributes.n2, 'v2'); + assert.strictEqual(p.assert.n2, 'v2'); + return new SourceTextModule(''); + }); +} + +const finished = common.mustCall(); + +(async function main() { + await simple(); + await invalidLinkValue(); + await depth(); + await circular(); + await circular2(); + await asserts(); + finished(); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-vm-sigint.js b/test/js/node/test/parallel/test-vm-sigint.js new file mode 100644 index 0000000000..66f22ba878 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-sigint.js @@ -0,0 +1,52 @@ +'use strict'; +const common = require('../common'); +if (common.isWindows) { + // No way to send CTRL_C_EVENT to processes from JS right now. + common.skip('platform not supported'); +} + +const assert = require('assert'); +const vm = require('vm'); +const spawn = require('child_process').spawn; + +if (process.argv[2] === 'child') { + const method = process.argv[3]; + const listeners = +process.argv[4]; + assert.ok(method); + assert.ok(Number.isInteger(listeners)); + + const script = `process.send('${method}'); while(true) {}`; + const args = method === 'runInContext' ? + [vm.createContext({ process })] : + []; + const options = { breakOnSigint: true }; + + for (let i = 0; i < listeners; i++) + process.on('SIGINT', common.mustNotCall()); + + assert.throws( + () => { vm[method](script, ...args, options); }, + { + code: 'ERR_SCRIPT_EXECUTION_INTERRUPTED', + message: 'Script execution was interrupted by `SIGINT`' + }); + return; +} + +for (const method of ['runInThisContext', 'runInContext']) { + for (const listeners of [0, 1, 2]) { + const args = [__filename, 'child', method, listeners]; + const child = spawn(process.execPath, args, { + stdio: [null, 'pipe', 'inherit', 'ipc'] + }); + + child.on('message', common.mustCall(() => { + process.kill(child.pid, 'SIGINT'); + })); + + child.on('close', common.mustCall((code, signal) => { + assert.strictEqual(signal, null); + assert.strictEqual(code, 0); + })); + } +} diff --git a/test/js/node/test/parallel/test-vm-timeout-escape-promise-module.js b/test/js/node/test/parallel/test-vm-timeout-escape-promise-module.js new file mode 100644 index 0000000000..4b0169a778 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-timeout-escape-promise-module.js @@ -0,0 +1,42 @@ +// Flags: --experimental-vm-modules +'use strict'; + +// https://github.com/nodejs/node/issues/3020 +// Promises used to allow code to escape the timeout +// set for runInContext, runInNewContext, and runInThisContext. + +const common = require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +const NS_PER_MS = 1000000n; + +const hrtime = process.hrtime.bigint; + +function loop() { + const start = hrtime(); + while (1) { + const current = hrtime(); + const span = (current - start) / NS_PER_MS; + if (span >= 2000n) { + throw new Error( + `escaped timeout at ${span} milliseconds!`); + } + } +} + +assert.rejects(async () => { + const module = new vm.SourceTextModule( + 'Promise.resolve().then(() => loop()); loop();', + { + context: vm.createContext({ + hrtime, + loop + }, { microtaskMode: 'afterEvaluate' }) + }); + await module.link(common.mustNotCall()); + await module.evaluate({ timeout: 5 }); +}, { + code: 'ERR_SCRIPT_EXECUTION_TIMEOUT', + message: 'Script execution timed out after 5ms' +}).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-vm-timeout-escape-promise.js b/test/js/node/test/parallel/test-vm-timeout-escape-promise.js new file mode 100644 index 0000000000..36a76001af --- /dev/null +++ b/test/js/node/test/parallel/test-vm-timeout-escape-promise.js @@ -0,0 +1,39 @@ +'use strict'; + +// https://github.com/nodejs/node/issues/3020 +// Promises used to allow code to escape the timeout +// set for runInContext, runInNewContext, and runInThisContext. + +require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +const NS_PER_MS = 1000000n; + +const hrtime = process.hrtime.bigint; + +function loop() { + const start = hrtime(); + while (1) { + const current = hrtime(); + const span = (current - start) / NS_PER_MS; + if (span >= 2000n) { + throw new Error( + `escaped timeout at ${span} milliseconds!`); + } + } +} + +assert.throws(() => { + vm.runInNewContext( + 'Promise.resolve().then(() => loop()); loop();', + { + hrtime, + loop + }, + { timeout: 5, microtaskMode: 'afterEvaluate' } + ); +}, { + code: 'ERR_SCRIPT_EXECUTION_TIMEOUT', + message: 'Script execution timed out after 5ms' +}); diff --git a/test/js/node/test/parallel/test-vm-timeout.js b/test/js/node/test/parallel/test-vm-timeout.js new file mode 100644 index 0000000000..426d1e0692 --- /dev/null +++ b/test/js/node/test/parallel/test-vm-timeout.js @@ -0,0 +1,81 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +// Timeout of 100ms executing endless loop +assert.throws( + function() { + vm.runInThisContext('while(true) {}', { timeout: 100 }); + }, + { + code: 'ERR_SCRIPT_EXECUTION_TIMEOUT', + message: 'Script execution timed out after 100ms' + }); + +// Timeout of 1000ms, script finishes first +vm.runInThisContext('', { timeout: 1000 }); + +// Nested vm timeouts, inner timeout propagates out +assert.throws( + function() { + const context = { + log: console.log, + runInVM: function(timeout) { + vm.runInNewContext('while(true) {}', context, { timeout }); + } + }; + vm.runInNewContext('runInVM(10)', context, { timeout: 10000 }); + throw new Error('Test 5 failed'); + }, + { + code: 'ERR_SCRIPT_EXECUTION_TIMEOUT', + message: 'Script execution timed out after 10ms' + }); + +// Nested vm timeouts, outer timeout is shorter and fires first. +assert.throws( + function() { + const context = { + runInVM: function(timeout) { + vm.runInNewContext('while(true) {}', context, { timeout }); + } + }; + vm.runInNewContext('runInVM(10000)', context, { timeout: 100 }); + throw new Error('Test 6 failed'); + }, + { + code: 'ERR_SCRIPT_EXECUTION_TIMEOUT', + message: 'Script execution timed out after 100ms' + }); + +// Nested vm timeouts, inner script throws an error. +assert.throws(function() { + const context = { + runInVM: function(timeout) { + vm.runInNewContext('throw new Error(\'foobar\')', context, { timeout }); + } + }; + vm.runInNewContext('runInVM(10000)', context, { timeout: 100000 }); +}, /foobar/);