diff --git a/src/bun.js/bindings/NodeVM.cpp b/src/bun.js/bindings/NodeVM.cpp index bcf870de112..6c54add7471 100644 --- a/src/bun.js/bindings/NodeVM.cpp +++ b/src/bun.js/bindings/NodeVM.cpp @@ -514,6 +514,23 @@ std::optional getNodeVMContextOptions(JSGlobalObject* globa JSValue codeGenerationValue = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, codeGenerationKey)); RETURN_IF_EXCEPTION(scope, {}); + auto microtaskModeValue = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "microtaskMode"_s)); + RETURN_IF_EXCEPTION(scope, {}); + if (microtaskModeValue) { + if (microtaskModeValue.isUndefined()) { + // ignore unset + } else if (!microtaskModeValue.isString()) { + return ERR::INVALID_ARG_TYPE(scope, globalObject, "options.microtaskMode"_s, "string"_s, microtaskModeValue); + } else { + StringView str = microtaskModeValue.toWTFString(globalObject); + if (str == "afterEvaluate") { + outOptions.microtaskMode = NodeVMContextOptions::MicrotaskMode::AfterEvaluate; + } else { + return INVALID_ARG_VALUE_VM_VARIATION(scope, globalObject, "options.microtaskMode must be 'afterEvaluate'"_s, microtaskModeValue); + } + } + } + if (codeGenerationValue) { if (codeGenerationValue.isUndefined()) { return std::nullopt; @@ -1072,6 +1089,10 @@ JSC_DEFINE_HOST_FUNCTION(vmModuleRunInNewContext, (JSGlobalObject * globalObject defaultGlobalObject(globalObject)->NodeVMGlobalObjectStructure(), contextOptions, globalObjectDynamicImportCallback); + if (contextOptions.microtaskMode == NodeVMContextOptions::MicrotaskMode::AfterEvaluate) { + context->createContextMicrotaskQueue(); + } + context->setContextifiedObject(sandbox); JSValue optionsArg = callFrame->argument(2); @@ -1099,6 +1120,9 @@ JSC_DEFINE_HOST_FUNCTION(vmModuleRunInNewContext, (JSGlobalObject * globalObject NakedPtr exception; JSValue result = JSC::evaluate(context, sourceCode, context, exception); + if (context->microtaskMode() == NodeVMContextOptions::MicrotaskMode::AfterEvaluate) { + context->drainMicrotasks(); + } if (exception) [[unlikely]] { if (handleException(globalObject, vm, exception, scope)) { @@ -1315,6 +1339,10 @@ JSC_DEFINE_HOST_FUNCTION(vmModule_createContext, (JSGlobalObject * globalObject, zigGlobalObject->NodeVMGlobalObjectStructure(), contextOptions, importer); + if (contextOptions.microtaskMode == NodeVMContextOptions::MicrotaskMode::AfterEvaluate) { + targetContext->createContextMicrotaskQueue(); + } + RETURN_IF_EXCEPTION(scope, {}); // Set sandbox as contextified object diff --git a/src/bun.js/bindings/NodeVM.h b/src/bun.js/bindings/NodeVM.h index 797af6aa4ff..7f31ed2bbdf 100644 --- a/src/bun.js/bindings/NodeVM.h +++ b/src/bun.js/bindings/NodeVM.h @@ -69,9 +69,15 @@ class CompileFunctionOptions : public BaseVMOptions { class NodeVMContextOptions final { public: + enum class MicrotaskMode : uint8_t { + Default, + AfterEvaluate, + }; + bool allowStrings = true; bool allowWasm = true; bool notContextified = false; + MicrotaskMode microtaskMode = MicrotaskMode::Default; }; class NodeVMGlobalObject; @@ -128,6 +134,7 @@ class NodeVMGlobalObject final : public Bun::GlobalScope { NodeVMSpecialSandbox* specialSandbox() const { return m_specialSandbox.get(); } void setSpecialSandbox(NodeVMSpecialSandbox* sandbox) { m_specialSandbox.set(vm(), this, sandbox); } JSValue dynamicImportCallback() const { return m_dynamicImportCallback.get(); } + NodeVMContextOptions::MicrotaskMode microtaskMode() const { return m_contextOptions.microtaskMode; } // Override property access to delegate to contextified object static bool getOwnPropertySlot(JSObject*, JSGlobalObject*, JSC::PropertyName, JSC::PropertySlot&); diff --git a/src/bun.js/bindings/NodeVMScript.cpp b/src/bun.js/bindings/NodeVMScript.cpp index e176d30b912..7b3eefcf73a 100644 --- a/src/bun.js/bindings/NodeVMScript.cpp +++ b/src/bun.js/bindings/NodeVMScript.cpp @@ -322,6 +322,16 @@ void setupWatchdog(VM& vm, double timeout, double* oldTimeout, double* newTimeou } } +static void runScript(JSGlobalObject* globalObject, NodeVMScript* script, NakedPtr& exception, JSValue& result) +{ + result = JSC::evaluate(globalObject, script->source(), globalObject, exception); + if (auto* nodeVmObj = JSC::jsDynamicCast(globalObject)) { + if (nodeVmObj->microtaskMode() == NodeVMContextOptions::MicrotaskMode::AfterEvaluate) { + globalObject->drainMicrotasks(); + } + } +} + static JSC::EncodedJSValue runInContext(NodeVMGlobalObject* globalObject, NodeVMScript* script, JSObject* contextifiedObject, JSValue optionsArg, bool allowStringInPlaceOfOptions = false) { VM& vm = JSC::getVM(globalObject); @@ -344,9 +354,6 @@ static JSC::EncodedJSValue runInContext(NodeVMGlobalObject* globalObject, NodeVM NakedPtr exception; JSValue result {}; - auto run = [&] { - result = JSC::evaluate(globalObject, script->source(), globalObject, exception); - }; std::optional oldLimit, newLimit; @@ -358,9 +365,9 @@ static JSC::EncodedJSValue runInContext(NodeVMGlobalObject* globalObject, NodeVM if (options.breakOnSigint) { auto holder = SigintWatcher::hold(globalObject, script); - run(); + runScript(globalObject, script, exception, result); } else { - run(); + runScript(globalObject, script, exception, result); } RETURN_IF_EXCEPTION(scope, {}); @@ -408,9 +415,6 @@ JSC_DEFINE_HOST_FUNCTION(scriptRunInThisContext, (JSGlobalObject * globalObject, NakedPtr exception; JSValue result {}; - auto run = [&] { - result = JSC::evaluate(globalObject, script->source(), globalObject, exception); - }; std::optional oldLimit, newLimit; @@ -423,9 +427,9 @@ JSC_DEFINE_HOST_FUNCTION(scriptRunInThisContext, (JSGlobalObject * globalObject, if (options.breakOnSigint) { auto holder = SigintWatcher::hold(globalObject, script); vm.ensureTerminationException(); - run(); + runScript(globalObject, script, exception, result); } else { - run(); + runScript(globalObject, script, exception, result); } if (options.timeout) { diff --git a/test/js/node/vm/vm-microtask-order.test.js b/test/js/node/vm/vm-microtask-order.test.js new file mode 100644 index 00000000000..1791d9b5ca6 --- /dev/null +++ b/test/js/node/vm/vm-microtask-order.test.js @@ -0,0 +1,32 @@ +const vm = require("node:vm"); + +const microtaskMode = process.argv[2]; + +let context; +if (microtaskMode === "undefined") { + context = vm.createContext({ console }); +} else { + context = vm.createContext({ console }, { microtaskMode }); +} + +const code = ` + Promise.resolve().then(() => { + console.log('Microtask inside VM'); + }); + + Promise.resolve().then(() => { + console.log('Microtask inside VM 2'); + }); + + console.log('End of VM code'); +`; + +console.log("Before vm.runInContext"); + +Promise.resolve().then(() => { + console.log("Microtask outside VM"); +}); + +vm.runInContext(code, context); + +console.log("After vm.runInContext"); diff --git a/test/js/node/vm/vm.test.ts b/test/js/node/vm/vm.test.ts index 3850870d58c..ed422aba693 100644 --- a/test/js/node/vm/vm.test.ts +++ b/test/js/node/vm/vm.test.ts @@ -1,3 +1,5 @@ +import { bunExe } from "harness"; +import { join } from "path"; import { describe, expect, test } from "bun:test"; import { compileFunction, createContext, runInContext, runInNewContext, runInThisContext, Script } from "node:vm"; @@ -172,9 +174,7 @@ describe("vm", () => { compileFunction( "with(Object.prototype) { toString = function() { globalThis.withHacked = true; }; } return 'test';", [], - { - parsingContext, - }, + { parsingContext }, )(); // Check that Object.prototype.toString wasn't modified @@ -505,6 +505,36 @@ function testRunInContext({ fn, isIsolated, isNew }: TestRunInContextArg) { }); expect(result).toContain("foo.js"); }); + test.each([ + { mode: "afterEvaluate", shouldFail: false }, + { mode: "undefined", shouldFail: false }, + { mode: "invalid", shouldFail: true }, + ])("microtaskMode: %o", async ({ mode, shouldFail }) => { + const runTest = async (runtime: string) => { + const args = [runtime, join(__dirname, "vm-microtask-order.test.js")]; + args.push(mode); + const proc = Bun.spawn(args, { + env: { + ...process.env, + NO_COLOR: "1", + }, + }); + const output = await new Response(proc.stdout).text(); + const exitCode = await proc.exited; + return { exitCode, output }; + }; + + const { exitCode: bunExitCode, output: bunOutput } = await runTest(bunExe()); + const cleanedBunOutput = bunOutput.replaceAll(/^\[\w+\].+$/gm, "").trim(); + + const { exitCode: nodeExitCode, output: nodeOutput } = await runTest("node"); + const cleanedNodeOutput = nodeOutput.trim(); + + expect(bunExitCode).toBe(nodeExitCode); + if (!shouldFail) { + expect(cleanedBunOutput).toEqual(cleanedNodeOutput); + } + }); } test.todo("can specify filename", () => { // @@ -539,10 +569,6 @@ function testRunInContext({ fn, isIsolated, isNew }: TestRunInContextArg) { test.todo("can specify contextOrigin", () => { // }); - // https://github.com/oven-sh/bun/issues/10885 .if(isNew == true) - test.todo("can specify microtaskMode", () => { - // - }); } function randomProp() {