Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/bun.js/bindings/NodeVM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,23 @@ std::optional<JSC::EncodedJSValue> 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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1099,6 +1120,9 @@ JSC_DEFINE_HOST_FUNCTION(vmModuleRunInNewContext, (JSGlobalObject * globalObject

NakedPtr<JSC::Exception> 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)) {
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/bun.js/bindings/NodeVM.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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&);
Expand Down
24 changes: 14 additions & 10 deletions src/bun.js/bindings/NodeVMScript.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,16 @@ void setupWatchdog(VM& vm, double timeout, double* oldTimeout, double* newTimeou
}
}

static void runScript(JSGlobalObject* globalObject, NodeVMScript* script, NakedPtr<JSC::Exception>& exception, JSValue& result)
{
result = JSC::evaluate(globalObject, script->source(), globalObject, exception);
if (auto* nodeVmObj = JSC::jsDynamicCast<NodeVMGlobalObject*>(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);
Expand All @@ -344,9 +354,6 @@ static JSC::EncodedJSValue runInContext(NodeVMGlobalObject* globalObject, NodeVM

NakedPtr<JSC::Exception> exception;
JSValue result {};
auto run = [&] {
result = JSC::evaluate(globalObject, script->source(), globalObject, exception);
};

std::optional<double> oldLimit, newLimit;

Expand All @@ -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, {});
Expand Down Expand Up @@ -408,9 +415,6 @@ JSC_DEFINE_HOST_FUNCTION(scriptRunInThisContext, (JSGlobalObject * globalObject,

NakedPtr<JSC::Exception> exception;
JSValue result {};
auto run = [&] {
result = JSC::evaluate(globalObject, script->source(), globalObject, exception);
};

std::optional<double> oldLimit, newLimit;

Expand All @@ -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) {
Expand Down
32 changes: 32 additions & 0 deletions test/js/node/vm/vm-microtask-order.test.js
Original file line number Diff line number Diff line change
@@ -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");
40 changes: 33 additions & 7 deletions test/js/node/vm/vm.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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", () => {
//
Expand Down Expand Up @@ -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() {
Expand Down