Compare commits

...

42 Commits

Author SHA1 Message Date
190n
0a0d4f0a38 Merge branch 'main' into ben/worker-stdio 2025-06-06 11:00:36 -07:00
Ben Grant
e90cfb3143 node tests 2025-06-03 15:32:02 -07:00
Ben Grant
3283c555bf Merge branch 'main' into ben/worker-stdio 2025-06-03 15:29:19 -07:00
Ben Grant
8920316c54 fix 2025-06-03 15:27:20 -07:00
Ben Grant
f7e59d1fc4 exception check 2025-06-03 15:27:15 -07:00
Ben Grant
bca78ac3a9 broken 2025-06-02 14:42:10 -07:00
Ben Grant
067dcab6d2 Handle stdin before worker starts 2025-06-02 13:58:59 -07:00
Ben Grant
89dbacab15 Merge branch 'main' into ben/worker-stdio 2025-06-02 11:18:17 -07:00
Ben Grant
e9503d3d13 WIP stdin 2025-05-30 18:25:16 -07:00
Ben Grant
5f45e9389b Merge branch 'main' into ben/worker-stdio 2025-05-30 12:51:09 -07:00
Ben Grant
e92096d05f remove todo 2025-05-29 14:22:23 -07:00
Ben Grant
b3f9fde022 Delete test-async-hooks-worker-asyncfn-terminate-4 2025-05-29 13:17:41 -07:00
Ben Grant
d84edf52b6 todo 2025-05-29 12:39:19 -07:00
Ben Grant
d465b98884 Merge branch 'main' into ben/worker-stdio 2025-05-29 12:32:40 -07:00
Ben Grant
19bb1600f9 node tests 2025-05-28 14:53:23 -07:00
Ben Grant
5bcc2a22cf Use process stream in worker thread 2025-05-28 14:39:10 -07:00
Ben Grant
aaa308a75d Remove unused option 2025-05-28 13:37:51 -07:00
Ben Grant
34509cbf1e Remove cast 2025-05-28 13:37:46 -07:00
Ben Grant
6d8d8dd0b6 Fix test in release 2025-05-28 13:23:31 -07:00
Ben Grant
35982fe167 Fix worker.test.ts in release and on Windows 2025-05-28 13:15:48 -07:00
Ben Grant
4a4816a3d3 Disable capturing in web workers 2025-05-28 12:30:51 -07:00
Ben Grant
215c52f786 Merge branch 'main' into ben/worker-stdio 2025-05-28 11:56:54 -07:00
Ben Grant
20baa2ca2c Fix console.trace coloring + more tests 2025-05-28 11:50:58 -07:00
Ben Grant
cda77ab831 1 stricter + 1 TODO console test 2025-05-27 18:06:29 -07:00
Ben Grant
326bf1c8fe fix enum names 2025-05-27 17:19:26 -07:00
Ben Grant
f852a94a92 redirect console in worker_threads 2025-05-27 17:11:19 -07:00
Ben Grant
d47929db2c Honor stdout/stderr options 2025-05-27 14:53:27 -07:00
Ben Grant
6c18ecdd4c fix test 2025-05-27 12:49:02 -07:00
190n
9ab949428f bun run prettier 2025-05-27 18:45:10 +00:00
Ben Grant
ad1abcef1c Merge branch 'main' into ben/worker-stdio 2025-05-27 11:42:27 -07:00
Ben Grant
02e539ef01 more tests 2025-05-27 11:27:33 -07:00
Ben Grant
fd1f4fdc73 Separate overrideProcessStdio 2025-05-23 18:36:33 -07:00
Ben Grant
398a1479e3 emit on worker property too 2025-05-23 17:50:02 -07:00
190n
b9feec098c bun run prettier 2025-05-23 22:47:25 +00:00
Ben Grant
934124fc1a WIP 2025-05-23 15:45:18 -07:00
Ben Grant
989617ee86 some tests 2025-05-23 15:45:11 -07:00
Ben Grant
5526d5c940 bump @types/node in test folder 2025-05-23 15:44:38 -07:00
190n
f44cc7d557 bun scripts/glob-sources.mjs 2025-05-23 19:37:24 +00:00
Ben Grant
fa91d1346c Merge branch 'main' into ben/worker-stdio 2025-05-23 12:32:33 -07:00
Ben Grant
657ac6a4a3 wip 2025-05-22 16:29:33 -07:00
Ben Grant
ea35263dad WIP 2025-05-21 17:59:55 -07:00
Ben Grant
2e225d5326 Add Zig::GlobalObject::worker() 2025-05-21 12:52:38 -07:00
27 changed files with 1145 additions and 140 deletions

View File

@@ -95,6 +95,7 @@ src/js/internal/util/inspect.js
src/js/internal/util/mime.ts
src/js/internal/validators.ts
src/js/internal/webstreams_adapters.ts
src/js/internal/worker_threads.ts
src/js/node/_http_agent.ts
src/js/node/_http_client.ts
src/js/node/_http_common.ts

View File

@@ -146,27 +146,42 @@ fn messageWithTypeAndLevel_(
return;
}
const enable_colors = global.bunVM().worker == null and
if (level == .Warning or level == .Error)
Output.enable_ansi_colors_stderr
else
Output.enable_ansi_colors_stdout;
const use_process_stdio = if (global.bunVM().worker) |w|
w.kind == .node
else
false;
var worker_writer = if (use_process_stdio)
global.processStdioWriter(if (level == .Warning or level == .Error) .stderr else .stdout)
else
null;
const underlying_writer = if (worker_writer) |*w|
w.any()
else if (level == .Warning or level == .Error) // TODO fix buffering here
console.error_writer.unbuffered_writer.any()
else
console.writer.unbuffered_writer.any();
var buffered_writer = std.io.bufferedWriter(underlying_writer);
var writer = buffered_writer.writer();
const Writer = @TypeOf(writer);
if (message_type == .Assert and len == 0) {
const text = if (Output.enable_ansi_colors_stderr)
const text = if (enable_colors)
Output.prettyFmt("<r><red>Assertion failed<r>\n", true)
else
"Assertion failed\n";
console.error_writer.unbuffered_writer.writeAll(text) catch {};
writer.writeAll(text) catch {};
buffered_writer.flush() catch {};
return;
}
const enable_colors = if (level == .Warning or level == .Error)
Output.enable_ansi_colors_stderr
else
Output.enable_ansi_colors_stdout;
var buffered_writer = if (level == .Warning or level == .Error)
&console.error_writer
else
&console.writer;
var writer = buffered_writer.writer();
const Writer = @TypeOf(writer);
var print_length = len;
var print_options: FormatOptions = .{
.enable_colors = enable_colors,
@@ -230,13 +245,13 @@ fn messageWithTypeAndLevel_(
print_options,
)
else if (message_type == .Log) {
_ = console.writer.write("\n") catch 0;
console.writer.flush() catch {};
_ = writer.write("\n") catch 0;
buffered_writer.flush() catch {};
} else if (message_type != .Trace)
writer.writeAll("undefined\n") catch {};
if (message_type == .Trace) {
writeTrace(Writer, writer, global);
writeTrace(Writer, writer, global, enable_colors);
buffered_writer.flush() catch {};
}
}
@@ -656,7 +671,7 @@ pub const TablePrinter = struct {
}
};
pub fn writeTrace(comptime Writer: type, writer: Writer, global: *JSGlobalObject) void {
pub fn writeTrace(comptime Writer: type, writer: Writer, global: *JSGlobalObject, enable_ansi_colors: bool) void {
var holder = ZigException.Holder.init();
var vm = VirtualMachine.get();
defer holder.deinit(vm);
@@ -676,20 +691,14 @@ pub fn writeTrace(comptime Writer: type, writer: Writer, global: *JSGlobalObject
false,
);
if (Output.enable_ansi_colors_stderr)
VirtualMachine.printStackTrace(
switch (enable_ansi_colors) {
inline else => |color| VirtualMachine.printStackTrace(
Writer,
writer,
exception.stack,
true,
) catch {}
else
VirtualMachine.printStackTrace(
Writer,
writer,
exception.stack,
false,
) catch {};
color,
) catch {},
}
}
pub const FormatOptions = struct {

View File

@@ -2023,6 +2023,11 @@ export fn Bun__VirtualMachine__setOverrideModuleRunMainPromise(vm: *VirtualMachi
}
}
export fn Bun__VirtualMachine__getWorker(vm: *VirtualMachine) ?*anyopaque {
const worker = vm.worker orelse return null;
return worker.cpp_worker;
}
pub fn reloadEntryPointForTestRunner(this: *VirtualMachine, entry_path: []const u8) !*JSInternalPromise {
this.has_loaded = false;
this.main = entry_path;

View File

@@ -132,7 +132,7 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool,
Output.flush();
if (!globalThis.hasException()) {
JSC.ConsoleObject.writeTrace(@TypeOf(&writer), &writer, globalThis);
JSC.ConsoleObject.writeTrace(@TypeOf(&writer), &writer, globalThis, Output.enable_ansi_colors_stderr);
}
Output.flush();
}

View File

@@ -2039,8 +2039,9 @@ enum class BunProcessStdinFdType : int32_t {
extern "C" BunProcessStdinFdType Bun__Process__getStdinFdType(void*, int fd);
extern "C" void Bun__ForceFileSinkToBeSynchronousForProcessObjectStdio(JSC::JSGlobalObject*, JSC::EncodedJSValue);
static JSValue constructStdioWriteStream(JSC::JSGlobalObject* globalObject, JSC::JSObject* processObject, int fd)
static JSValue constructStdioWriteStream(JSC::JSGlobalObject* jsGlobalObject, JSC::JSObject* processObject, int fd)
{
auto* globalObject = defaultGlobalObject(jsGlobalObject);
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_CATCH_SCOPE(vm);
@@ -2051,6 +2052,9 @@ static JSValue constructStdioWriteStream(JSC::JSGlobalObject* globalObject, JSC:
args.append(jsBoolean(bun_stdio_tty[fd]));
BunProcessStdinFdType fdType = Bun__Process__getStdinFdType(Bun::vm(vm), fd);
args.append(jsNumber(static_cast<int32_t>(fdType)));
bool isNodeWorkerThread = globalObject->worker()
&& globalObject->worker()->options().kind == WorkerOptions::Kind::Node;
args.append(jsBoolean(isNodeWorkerThread));
JSC::CallData callData = JSC::getCallData(getStdioWriteStream);
@@ -2082,7 +2086,10 @@ static JSValue constructStdioWriteStream(JSC::JSGlobalObject* globalObject, JSC:
forceSync = true;
#endif
if (forceSync) {
Bun__ForceFileSinkToBeSynchronousForProcessObjectStdio(globalObject, JSValue::encode(resultObject->getIndex(globalObject, 1)));
JSValue underlyingSink = resultObject->getIndex(globalObject, 1);
if (!underlyingSink.isUndefined()) {
Bun__ForceFileSinkToBeSynchronousForProcessObjectStdio(globalObject, JSValue::encode(underlyingSink));
}
}
return resultObject->getIndex(globalObject, 0);
@@ -2104,7 +2111,7 @@ static JSValue constructStderr(VM& vm, JSObject* processObject)
static JSValue constructStdin(VM& vm, JSObject* processObject)
{
auto* globalObject = processObject->globalObject();
auto* globalObject = defaultGlobalObject(processObject->globalObject());
auto scope = DECLARE_CATCH_SCOPE(vm);
JSC::JSFunction* getStdinStream = JSC::JSFunction::create(vm, globalObject, processObjectInternalsGetStdinStreamCodeGenerator(vm), globalObject);
JSC::MarkedArgumentBuffer args;
@@ -2113,6 +2120,9 @@ static JSValue constructStdin(VM& vm, JSObject* processObject)
args.append(jsBoolean(bun_stdio_tty[STDIN_FILENO]));
BunProcessStdinFdType fdType = Bun__Process__getStdinFdType(Bun::vm(vm), STDIN_FILENO);
args.append(jsNumber(static_cast<int32_t>(fdType)));
bool isNodeWorkerThread = globalObject->worker()
&& globalObject->worker()->options().kind == WorkerOptions::Kind::Node;
args.append(jsBoolean(isNodeWorkerThread));
JSC::CallData callData = JSC::getCallData(getStdinStream);
auto result = JSC::profiledCall(globalObject, ProfilingReason::API, getStdinStream, callData, globalObject, args);
@@ -2687,6 +2697,7 @@ void Process::visitChildrenImpl(JSCell* cell, Visitor& visitor)
thisObject->m_bindingUV.visit(visitor);
thisObject->m_bindingNatives.visit(visitor);
thisObject->m_emitHelperFunction.visit(visitor);
thisObject->m_emitWorkerStdioInParentFunction.visit(visitor);
}
DEFINE_VISIT_CHILDREN(Process);
@@ -3299,6 +3310,17 @@ JSValue Process::constructNextTickFn(JSC::VM& vm, Zig::GlobalObject* globalObjec
return nextTickFunction;
}
void Process::emitWorkerStdioInParent(JSWorker* worker, Worker::PushStdioFd fd, JSUint8Array* data)
{
auto* fn = m_emitWorkerStdioInParentFunction.getInitializedOnMainThread(this);
auto callData = JSC::getCallData(fn);
MarkedArgumentBuffer args;
args.append(worker);
args.append(jsNumber(static_cast<int>(fd)));
args.append(data);
JSC::call(globalObject(), fn, callData, jsNull(), args);
}
static JSValue constructProcessNextTickFn(VM& vm, JSObject* processObject)
{
JSGlobalObject* lexicalGlobalObject = processObject->globalObject();
@@ -3606,6 +3628,30 @@ extern "C" void Process__emitErrorEvent(Zig::GlobalObject* global, EncodedJSValu
}
}
extern "C" void Process__writeToStdio(JSGlobalObject* jsGlobalObject, int fd, const uint8_t* bytes, size_t len)
{
ASSERT(fd == 1 || fd == 2);
auto* globalObject = defaultGlobalObject(jsGlobalObject);
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
auto* process = globalObject->processObject();
auto stream = process->get(globalObject, Identifier::fromString(vm, fd == 1 ? "stdout"_s : "stderr"_s));
RETURN_IF_EXCEPTION(scope, );
auto writeFn = stream.get(globalObject, Identifier::fromString(vm, "write"_s));
RETURN_IF_EXCEPTION(scope, );
auto callData = JSC::getCallData(writeFn);
if (callData.type == CallData::Type::None) {
scope.throwException(globalObject, createNotAFunctionError(globalObject, writeFn));
return;
}
MarkedArgumentBuffer args;
auto* buffer = WebCore::createBuffer(globalObject, { bytes, len });
args.append(buffer);
JSC::call(globalObject, writeFn, callData, stream, args);
RETURN_IF_EXCEPTION(scope, );
}
/* Source for Process.lut.h
@begin processObjectTable
_debugEnd Process_stubEmptyFunction Function 0
@@ -3729,6 +3775,9 @@ void Process::finishCreation(JSC::VM& vm)
m_emitHelperFunction.initLater([](const JSC::LazyProperty<Process, JSFunction>::Initializer& init) {
init.set(JSFunction::create(init.vm, init.owner->globalObject(), 2, "emit"_s, Process_functionEmitHelper, ImplementationVisibility::Private));
});
m_emitWorkerStdioInParentFunction.initLater([](const JSC::LazyProperty<Process, JSFunction>::Initializer& init) {
init.set(JSFunction::create(init.vm, init.owner->globalObject(), processObjectInternalsEmitWorkerStdioInParentCodeGenerator(init.vm), init.owner->globalObject()));
});
putDirect(vm, vm.propertyNames->toStringTagSymbol, jsString(vm, String("process"_s)), 0);
putDirect(vm, Identifier::fromString(vm, "_exiting"_s), jsBoolean(false), 0);

View File

@@ -5,6 +5,7 @@
#include "BunBuiltinNames.h"
#include "BunClientData.h"
#include "JSEventEmitter.h"
#include "JSWorker.h"
namespace Zig {
class GlobalObject;
@@ -26,6 +27,10 @@ class Process : public WebCore::JSEventEmitter {
// Function that looks up "emit" on "process" and calls it with the provided arguments
// Only used by internal code via passing to queueNextTick
LazyProperty<Process, JSFunction> m_emitHelperFunction;
// Function called when a Worker thread sends data from its stdout or stderr stream, to emit
// the data in the main thread on the Worker object and possibly (depending on Worker options)
// the process stream
LazyProperty<Process, JSFunction> m_emitWorkerStdioInParentFunction;
WriteBarrier<Unknown> m_uncaughtExceptionCaptureCallback;
WriteBarrier<JSObject> m_nextTickFunction;
// https://github.com/nodejs/node/blob/2eff28fb7a93d3f672f80b582f664a7c701569fb/lib/internal/bootstrap/switches/does_own_process_state.js#L113-L116
@@ -78,6 +83,8 @@ public:
JSValue getExecArgv(JSGlobalObject* globalObject);
void setExecArgv(JSGlobalObject* globalObject, JSValue execArgv);
void emitWorkerStdioInParent(JSWorker* worker, Worker::PushStdioFd fd, JSUint8Array* data);
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject,
JSC::JSValue prototype)
{

View File

@@ -882,6 +882,36 @@ pub const JSGlobalObject = opaque {
return @enumFromInt(ScriptExecutionContextIdentifier__forGlobalObject(global));
}
pub const StdioWriterFd = enum(c_int) {
stdout = 1,
stderr = 2,
};
extern fn Process__writeToStdio(global: *JSGlobalObject, fd: StdioWriterFd, bytes: [*]const u8, len: usize) void;
const ProcessStdioWriterContext = struct {
global: *JSGlobalObject,
fd: StdioWriterFd,
fn write(this: ProcessStdioWriterContext, bytes: []const u8) bun.JSError!usize {
Process__writeToStdio(this.global, this.fd, bytes.ptr, bytes.len);
if (this.global.hasException()) {
return error.JSError;
}
return bytes.len;
}
};
pub const ProcessStdioWriter = std.io.Writer(
ProcessStdioWriterContext,
bun.JSError,
ProcessStdioWriterContext.write,
);
/// Get a writer which calls process.stdout.write or process.stderr.write
pub fn processStdioWriter(global: *JSGlobalObject, fd: StdioWriterFd) ProcessStdioWriter {
return .{ .context = .{ .global = global, .fd = fd } };
}
pub const Extern = [_][]const u8{ "create", "getModuleRegistryMap", "resetModuleRegistryMap" };
comptime {

View File

@@ -4410,6 +4410,14 @@ bool GlobalObject::hasNapiFinalizers() const
void GlobalObject::setNodeWorkerEnvironmentData(JSMap* data) { m_nodeWorkerEnvironmentData.set(vm(), this, data); }
extern "C" WebCore::Worker* Bun__VirtualMachine__getWorker(VirtualMachine* bunVM);
WebCore::Worker* GlobalObject::worker()
{
// TODO make bunVM typed instead of void* everywhere
return Bun__VirtualMachine__getWorker(reinterpret_cast<VirtualMachine*>(bunVM()));
}
void GlobalObject::trackFFIFunction(JSC::JSFunction* function)
{
this->m_ffiFunctions.append(JSC::Strong<JSC::JSFunction> { vm(), function });

View File

@@ -25,6 +25,7 @@ class WorkerGlobalScope;
class SubtleCrypto;
class EventTarget;
class Performance;
class Worker;
} // namespace WebCore
namespace Bun {
@@ -398,6 +399,10 @@ public:
return func;
}
// Return the Worker object if this global object is running in a worker, or nullptr
// if it is not.
WebCore::Worker* worker();
bool asyncHooksNeedsCleanup = false;
double INSPECT_MAX_BYTES = 50;
bool isInsideErrorPrepareStackTraceCallback = false;

View File

@@ -64,6 +64,8 @@
#include "CloseEvent.h"
#include "JSMessagePort.h"
#include "JSBroadcastChannel.h"
#include "JSWorker.h"
#include "BunProcess.h"
namespace WebCore {
@@ -126,7 +128,8 @@ extern "C" void* WebWorker__create(
StringImpl** execArgvPtr,
size_t execArgvLen,
BunString* preloadModulesPtr,
size_t preloadModulesLen);
size_t preloadModulesLen,
WorkerOptions::Kind kind);
extern "C" void WebWorker__setRef(
void* worker,
bool ref);
@@ -206,7 +209,8 @@ ExceptionOr<Ref<Worker>> Worker::create(ScriptExecutionContext& context, const S
execArgv.data(),
execArgv.size(),
preloadModules.data(),
preloadModules.size());
preloadModules.size(),
options.kind);
// now referenced by Zig
worker->ref();
@@ -519,8 +523,6 @@ extern "C" void WebWorker__dispatchError(Zig::GlobalObject* globalObject, Worker
}
}
extern "C" WebCore::Worker* WebWorker__getParentWorker(void* bunVM);
JSC_DEFINE_HOST_FUNCTION(jsReceiveMessageOnPort, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
@@ -547,6 +549,77 @@ JSC_DEFINE_HOST_FUNCTION(jsReceiveMessageOnPort, (JSGlobalObject * lexicalGlobal
return Bun::throwError(lexicalGlobalObject, scope, Bun::ErrorCode::ERR_INVALID_ARG_TYPE, "The \"port\" argument must be a MessagePort instance"_s);
}
void Worker::pushStdio(Worker& child, PushStdioFd fd, std::span<const uint8_t> bytes)
{
Vector<uint8_t, 64> vec { bytes };
auto task = [worker = Ref { child }, fd, vec = WTFMove(vec)](ScriptExecutionContext& ctx) {
auto& vm = ctx.vm();
auto* destGlobalObject = defaultGlobalObject(ctx.globalObject());
auto scope = DECLARE_THROW_SCOPE(vm);
auto reportUncaught = [&destGlobalObject, &scope]() -> void {
destGlobalObject->reportUncaughtExceptionAtEventLoop(destGlobalObject, scope.exception());
};
auto* buffer = WebCore::createBuffer(destGlobalObject, vec.span());
if (fd == PushStdioFd::Stdin) {
auto* process = destGlobalObject->processObject();
auto processStdin = process->get(destGlobalObject, Identifier::fromString(vm, "stdin"_s));
RETURN_IF_EXCEPTION(scope, reportUncaught());
auto push = processStdin.get(destGlobalObject, Identifier::fromString(vm, "push"_s));
RETURN_IF_EXCEPTION(scope, reportUncaught());
auto callData = JSC::getCallData(push);
if (callData.type == CallData::Type::None) {
scope.throwException(destGlobalObject, createNotAFunctionError(destGlobalObject, push));
RETURN_IF_EXCEPTION(scope, reportUncaught());
}
MarkedArgumentBuffer args;
args.append(buffer);
JSC::call(destGlobalObject, push, callData, processStdin, args);
RETURN_IF_EXCEPTION(scope, reportUncaught());
} else {
auto* jsWorker = jsCast<JSWorker*>(WebCore::toJS(destGlobalObject, destGlobalObject, worker));
destGlobalObject->processObject()->emitWorkerStdioInParent(jsWorker, fd, buffer);
RETURN_IF_EXCEPTION(scope, reportUncaught());
}
};
if (fd == PushStdioFd::Stdin) {
child.postTaskToWorkerGlobalScope(WTFMove(task));
} else {
child.scriptExecutionContext()->postTaskConcurrently(WTFMove(task));
}
}
JSC_DEFINE_HOST_FUNCTION(jsPushStdioToParent, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
{
auto* globalObject = defaultGlobalObject(lexicalGlobalObject);
// internal binding
ASSERT(callFrame->argumentCount() == 2);
auto fd = callFrame->argument(0).asInt32();
ASSERT(fd == 1 || fd == 2);
auto* buf = jsCast<JSUint8Array*>(callFrame->argument(1));
auto& sourceWorker = *globalObject->worker();
Worker::pushStdio(sourceWorker, static_cast<Worker::PushStdioFd>(fd), buf->span());
return JSValue::encode(jsUndefined());
}
JSC_DEFINE_HOST_FUNCTION(jsPushStdinToChild, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
{
// internal binding
ASSERT(callFrame->argumentCount() == 2);
auto* destWorker = jsCast<JSWorker*>(callFrame->argument(0));
auto* buf = jsCast<JSUint8Array*>(callFrame->argument(1));
Worker::pushStdio(destWorker->wrapped(), Worker::PushStdioFd::Stdin, buf->span());
return JSValue::encode(jsUndefined());
}
JSValue createNodeWorkerThreadsBinding(Zig::GlobalObject* globalObject)
{
VM& vm = globalObject->vm();
@@ -556,7 +629,7 @@ JSValue createNodeWorkerThreadsBinding(Zig::GlobalObject* globalObject)
JSValue threadId = jsNumber(0);
JSMap* environmentData = nullptr;
if (auto* worker = WebWorker__getParentWorker(globalObject->bunVM())) {
if (auto* worker = globalObject->worker()) {
auto& options = worker->options();
auto ports = MessagePort::entanglePorts(*ScriptExecutionContext::getScriptExecutionContext(worker->clientIdentifier()), WTFMove(options.dataMessagePorts));
RefPtr<WebCore::SerializedScriptValue> serialized = WTFMove(options.workerDataAndEnvironmentData);
@@ -583,12 +656,14 @@ JSValue createNodeWorkerThreadsBinding(Zig::GlobalObject* globalObject)
ASSERT(environmentData);
globalObject->setNodeWorkerEnvironmentData(environmentData);
JSObject* array = constructEmptyArray(globalObject, nullptr, 4);
JSObject* array = constructEmptyArray(globalObject, nullptr, 5);
RETURN_IF_EXCEPTION(scope, {});
array->putDirectIndex(globalObject, 0, workerData);
array->putDirectIndex(globalObject, 1, threadId);
array->putDirectIndex(globalObject, 2, JSFunction::create(vm, globalObject, 1, "receiveMessageOnPort"_s, jsReceiveMessageOnPort, ImplementationVisibility::Public, NoIntrinsic));
array->putDirectIndex(globalObject, 3, environmentData);
array->putDirectIndex(globalObject, 4, JSFunction::create(vm, globalObject, 2, "pushStdioToParent"_s, jsPushStdioToParent, ImplementationVisibility::Public));
array->putDirectIndex(globalObject, 5, JSFunction::create(vm, globalObject, 2, "pushStdinToChild"_s, jsPushStdinToChild, ImplementationVisibility::Public));
return array;
}
@@ -602,7 +677,7 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionPostMessage,
if (!globalObject) [[unlikely]]
return JSValue::encode(jsUndefined());
Worker* worker = WebWorker__getParentWorker(globalObject->bunVM());
Worker* worker = globalObject->worker();
if (worker == nullptr)
return JSValue::encode(jsUndefined());

View File

@@ -89,10 +89,17 @@ public:
// true if successful
bool dispatchErrorWithValue(Zig::GlobalObject* workerGlobalObject, JSValue value);
void dispatchExit(int32_t exitCode);
// Returns the context of the thread that started this Worker
ScriptExecutionContext* scriptExecutionContext() const final { return ContextDestructionObserver::scriptExecutionContext(); }
ScriptExecutionContextIdentifier clientIdentifier() const { return m_clientIdentifier; }
WorkerOptions& options() { return m_options; }
enum class PushStdioFd : int {
Stdin = 0,
Stdout = 1,
Stderr = 2,
};
private:
Worker(ScriptExecutionContext&, WorkerOptions&&);
@@ -101,6 +108,11 @@ private:
void derefEventTarget() final { deref(); }
void eventListenersDidChange() final {};
// bytes are cloned
// for fd == Stdin: send from parent to child
// for fd == Stdout or Stderr: send from child to parent
static void pushStdio(Worker& child, PushStdioFd fd, std::span<const uint8_t> bytes);
static void networkStateChanged(bool isOnLine);
static constexpr uint8_t OnlineFlag = 1 << 0;
@@ -120,10 +132,15 @@ private:
std::atomic<uint8_t> m_terminationFlags { 0 };
const ScriptExecutionContextIdentifier m_clientIdentifier;
void* impl_ { nullptr };
friend JSC_DECLARE_HOST_FUNCTION(jsPushStdioToParent);
friend JSC_DECLARE_HOST_FUNCTION(jsPushStdinToChild);
};
JSValue createNodeWorkerThreadsBinding(Zig::GlobalObject* globalObject);
JSC_DECLARE_HOST_FUNCTION(jsFunctionPostMessage);
JSC_DECLARE_HOST_FUNCTION(jsPushStdioToParent);
JSC_DECLARE_HOST_FUNCTION(jsPushStdinToChild);
} // namespace WebCore

View File

@@ -24,7 +24,7 @@ preloads: [][]const u8 = &.{},
store_fd: bool = false,
arena: ?bun.MimallocArena = null,
name: [:0]const u8 = "Worker",
cpp_worker: *anyopaque,
cpp_worker: *CppWorker,
mini: bool,
// Most of our code doesn't care whether `eval` was passed, because worker_threads.ts
// automatically passes a Blob URL instead of a file path if `eval` is true. But, if `eval` is
@@ -44,6 +44,18 @@ execArgv: ?[]const WTFStringImpl,
/// Used to distinguish between terminate() called by exit(), and terminate() called for other reasons
exit_called: bool = false,
kind: Kind,
/// class WebCore::Worker
const CppWorker = opaque {};
/// enum class Kind in WorkerOptions.h
pub const Kind = enum(u8) {
/// Created by the global Worker constructor
web,
/// Created by the `require("node:worker_threads").Worker` constructor
node,
};
pub const Status = enum(u8) {
start,
@@ -52,15 +64,10 @@ pub const Status = enum(u8) {
terminated,
};
extern fn WebWorker__dispatchExit(?*jsc.JSGlobalObject, *anyopaque, i32) void;
extern fn WebWorker__dispatchOnline(cpp_worker: *anyopaque, *jsc.JSGlobalObject) void;
extern fn WebWorker__fireEarlyMessages(cpp_worker: *anyopaque, *jsc.JSGlobalObject) void;
extern fn WebWorker__dispatchError(*jsc.JSGlobalObject, *anyopaque, bun.String, JSValue) void;
export fn WebWorker__getParentWorker(vm: *jsc.VirtualMachine) ?*anyopaque {
const worker = vm.worker orelse return null;
return worker.cpp_worker;
}
extern fn WebWorker__dispatchExit(?*jsc.JSGlobalObject, *CppWorker, i32) void;
extern fn WebWorker__dispatchOnline(cpp_worker: *CppWorker, *jsc.JSGlobalObject) void;
extern fn WebWorker__fireEarlyMessages(cpp_worker: *CppWorker, *jsc.JSGlobalObject) void;
extern fn WebWorker__dispatchError(*jsc.JSGlobalObject, *CppWorker, bun.String, JSValue) void;
pub fn hasRequestedTerminate(this: *const WebWorker) bool {
return this.requested_terminate.load(.monotonic);
@@ -70,7 +77,7 @@ pub fn setRequestedTerminate(this: *WebWorker) bool {
return this.requested_terminate.swap(true, .release);
}
export fn WebWorker__updatePtr(worker: *WebWorker, ptr: *anyopaque) bool {
export fn WebWorker__updatePtr(worker: *WebWorker, ptr: *CppWorker) bool {
worker.cpp_worker = ptr;
var thread = std.Thread.spawn(
@@ -177,7 +184,7 @@ fn resolveEntryPointSpecifier(
}
pub fn create(
cpp_worker: *void,
cpp_worker: *CppWorker,
parent: *jsc.VirtualMachine,
name_str: bun.String,
specifier_str: bun.String,
@@ -194,6 +201,7 @@ pub fn create(
execArgv_len: usize,
preload_modules_ptr: ?[*]bun.String,
preload_modules_len: usize,
kind: Kind,
) callconv(.c) ?*WebWorker {
jsc.markBinding(@src());
log("[{d}] WebWorker.create", .{this_context_id});
@@ -245,6 +253,7 @@ pub fn create(
.argv = if (argv_ptr) |ptr| ptr[0..argv_len] else &.{},
.execArgv = if (inherit_execArgv) null else (if (execArgv_ptr) |ptr| ptr[0..execArgv_len] else &.{}),
.preloads = preloads.items,
.kind = kind,
};
worker.parent_poll_ref.ref(parent);
@@ -391,7 +400,6 @@ fn onUnhandledRejection(vm: *jsc.VirtualMachine, globalObject: *jsc.JSGlobalObje
var worker = vm.worker orelse @panic("Assertion failure: no worker");
const writer = buffered_writer.writer();
const Writer = @TypeOf(writer);
// we buffer this because it'll almost always be < 4096
// when it's under 4096, we want to avoid the dynamic allocation
jsc.ConsoleObject.format2(
@@ -399,8 +407,8 @@ fn onUnhandledRejection(vm: *jsc.VirtualMachine, globalObject: *jsc.JSGlobalObje
globalObject,
&[_]jsc.JSValue{error_instance},
1,
Writer,
Writer,
@TypeOf(writer),
@TypeOf(writer),
writer,
.{
.enable_colors = false,

View File

@@ -35,47 +35,52 @@ export function getStdioWriteStream(
fd: number,
isTTY: boolean,
_fdType: BunProcessStdinFdType,
isNodeWorkerThread: boolean,
) {
$assert(fd === 1 || fd === 2, `Expected fd to be 1 or 2, got ${fd}`);
let stream;
if (isTTY) {
const tty = require("node:tty");
stream = new tty.WriteStream(fd);
// TODO: this is the wrong place for this property.
// but the TTY is technically duplex
// see test-fs-syncwritestream.js
stream.readable = true;
process.on("SIGWINCH", () => {
stream._refreshSize();
});
stream._type = "tty";
// workers do not handle stdin yet
if (!isNodeWorkerThread) {
if (isTTY) {
const tty = require("node:tty");
stream = new tty.WriteStream(fd);
// TODO: this is the wrong place for this property.
// but the TTY is technically duplex
// see test-fs-syncwritestream.js
stream.readable = true;
process.on("SIGWINCH", () => {
stream._refreshSize();
});
stream._type = "tty";
} else {
const fs = require("node:fs");
stream = new fs.WriteStream(null, { autoClose: false, fd, $fastPath: true });
stream.readable = false;
stream._type = "fs";
}
if (fd === 1 || fd === 2) {
stream.destroySoon = stream.destroy;
stream._destroy = function (err, cb) {
cb(err);
this._undestroy();
if (!this._writableState.emitClose) {
process.nextTick(() => {
this.emit("close");
});
}
};
}
stream._isStdio = true;
stream.fd = fd;
} else {
const fs = require("node:fs");
stream = new fs.WriteStream(null, { autoClose: false, fd, $fastPath: true });
stream.readable = false;
stream._type = "fs";
stream = new (require("internal/worker_threads").WritableWorkerStdio)(fd);
}
if (fd === 1 || fd === 2) {
stream.destroySoon = stream.destroy;
stream._destroy = function (err, cb) {
cb(err);
this._undestroy();
if (!this._writableState.emitClose) {
process.nextTick(() => {
this.emit("close");
});
}
};
}
stream._isStdio = true;
stream.fd = fd;
const underlyingSink = stream[require("internal/fs/streams").kWriteStreamFastPath];
$assert(underlyingSink);
return [stream, underlyingSink];
}
@@ -84,6 +89,7 @@ export function getStdinStream(
fd: number,
isTTY: boolean,
fdType: BunProcessStdinFdType,
isNodeWorkerThread: boolean,
) {
$assert(fd === 0);
const native = Bun.stdin.stream();
@@ -126,6 +132,8 @@ export function getStdinStream(
}
}
if (isNodeWorkerThread) return new (require("internal/worker_threads").ReadableWorkerStdio)();
const ReadStream = isTTY ? require("node:tty").ReadStream : require("node:fs").ReadStream;
const stream = new ReadStream(null, { fd, autoClose: false });
@@ -149,7 +157,7 @@ export function getStdinStream(
return originalOn.$call(this, event, listener);
};
stream.fd = fd;
if (!isNodeWorkerThread) stream.fd = fd;
// tty.ReadStream is supposed to extend from net.Socket.
// but we haven't made that work yet. Until then, we need to manually add some of net.Socket's methods
@@ -451,3 +459,17 @@ export function getChannel() {
}
})();
}
export function emitWorkerStdioInParent(worker: Worker, fd: number, data: Buffer) {
$assert(fd === 1 || fd === 2);
const { webWorkerToStdio } = require("internal/worker_threads");
const streams = webWorkerToStdio.get(worker);
switch (fd) {
case 1:
streams?.stdout.push(data);
break;
case 2:
streams?.stderr.push(data);
break;
}
}

View File

@@ -0,0 +1,86 @@
const { Readable, Writable } = require("node:stream");
const {
0: _workerData,
1: _threadId,
2: _receiveMessageOnPort,
3: environmentData,
4: pushStdioToParent,
5: pushStdinToChild,
} = $cpp("Worker.cpp", "createNodeWorkerThreadsBinding") as [
unknown,
number,
(port: unknown) => unknown,
Map<unknown, unknown>,
(fd: number, data: Buffer) => void,
(worker: globalThis.Worker, data: Buffer) => void,
];
// Class exposed as `process.stdout` and `process.stderr` in Worker threads, and `worker.stdin` in the parent thread
class WritableWorkerStdio extends Writable {
#fd: number;
// `undefined` for output streams in the worker thread
// Worker instance for stdin stream in the parent thread
#worker: Worker | undefined;
constructor(fd: number, worker?: Worker) {
super();
$assert(worker === undefined || fd === 0);
this.#fd = fd;
this.#worker = worker;
if (worker) {
this.on("close", () => {
// process.stdin.push(null) in worker
});
}
}
_write(chunk: unknown, encoding: string, callback: (error?: Error | null) => void): void {
$assert(chunk instanceof Buffer);
$assert(encoding === "buffer");
if (this.#worker) {
pushStdinToChild(this.#worker, chunk);
} else {
pushStdioToParent(this.#fd, chunk);
}
callback();
}
}
// Class exposed as `worker.stdout` and `worker.stderr` in the parent thread, and `process.stdin` in the Worker thread
class ReadableWorkerStdio extends Readable {
constructor(worker?: Worker) {
super();
if (worker) {
worker.addEventListener("close", () => {
this.push(null);
});
} else {
// needs to push null when parent thread calls end() on stdin
}
}
_read() {}
}
// Map to access stdio-related options from an internal Web Worker object (not a worker_threads Worker)
const webWorkerToStdio = new WeakMap<
globalThis.Worker,
{
// stdout stream exposed in the parent thread
stdout: ReadableWorkerStdio;
// stderr stream exposed in the parent thread
stderr: ReadableWorkerStdio;
}
>();
export default {
WritableWorkerStdio,
ReadableWorkerStdio,
_workerData,
_threadId,
_receiveMessageOnPort,
environmentData,
webWorkerToStdio,
};

View File

@@ -21,16 +21,14 @@ const SHARE_ENV = Symbol("nodejs.worker_threads.SHARE_ENV");
const isMainThread = Bun.isMainThread;
const {
0: _workerData,
1: _threadId,
2: _receiveMessageOnPort,
3: environmentData,
} = $cpp("Worker.cpp", "createNodeWorkerThreadsBinding") as [
unknown,
number,
(port: unknown) => unknown,
Map<unknown, unknown>,
];
_workerData,
_threadId,
_receiveMessageOnPort,
environmentData,
webWorkerToStdio,
ReadableWorkerStdio,
WritableWorkerStdio,
} = require("internal/worker_threads");
type NodeWorkerOptions = import("node:worker_threads").WorkerOptions;
@@ -223,11 +221,14 @@ function moveMessagePortToContext() {
throwNotImplemented("worker_threads.moveMessagePortToContext");
}
const unsupportedOptions = ["stdin", "stdout", "stderr", "trackedUnmanagedFds", "resourceLimits"];
const unsupportedOptions = ["trackedUnmanagedFds", "resourceLimits"];
class Worker extends EventEmitter {
#worker: WebWorker;
#performance;
#stdin: InstanceType<typeof WritableWorkerStdio> | null;
#stdout: InstanceType<typeof ReadableWorkerStdio>;
#stderr: InstanceType<typeof ReadableWorkerStdio>;
// this is used by terminate();
// either is the exit code if exited, a promise resolving to the exit code, or undefined if we haven't sent .terminate() yet
@@ -277,6 +278,23 @@ class Worker extends EventEmitter {
}
urlRevokeRegistry.register(this.#worker, this.#urlToRevoke);
}
this.#stdout = new ReadableWorkerStdio(this.#worker);
this.#stderr = new ReadableWorkerStdio(this.#worker);
// TODO maybe bump max listeners?
if (!options.stdout) {
this.#stdout.pipe(process.stdout);
}
if (!options.stderr) {
this.#stderr.pipe(process.stderr);
}
if (options.stdin) {
this.#stdin = new WritableWorkerStdio(0, this.#worker);
} else {
this.#stdin = null;
}
webWorkerToStdio.set(this.#worker, { stdout: this.#stdout, stderr: this.#stderr });
}
get threadId() {
@@ -292,18 +310,15 @@ class Worker extends EventEmitter {
}
get stdin() {
// TODO:
return null;
return this.#stdin;
}
get stdout() {
// TODO:
return null;
return this.#stdout;
}
get stderr() {
// TODO:
return null;
return this.#stderr;
}
get performance() {

View File

@@ -93,6 +93,7 @@
"yargs": "17.7.2",
},
"devDependencies": {
"@types/node": "22.6.0",
"@types/puppeteer": "7.0.4",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
@@ -686,7 +687,7 @@
"@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="],
"@types/node": ["@types/node@20.14.6", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw=="],
"@types/node": ["@types/node@22.6.0", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-QyR8d5bmq+eR72TwQDfujwShHMcIrWIYsaQFtXRE58MHPTEKUNxjxvl0yS0qPMds5xbSDWtp7ZpvGFtd7dfMdQ=="],
"@types/oboe": ["@types/oboe@2.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-bXt4BXSQy0N/buSIak1o0TjYAk2SAeK1aZV9xKcb+xVGWYP8NcMOFy2T7Um3kIvEcQJzrdgJ8R6fpbRcp/LEww=="],
@@ -2484,7 +2485,7 @@
"undici": ["undici@5.20.0", "", { "dependencies": { "busboy": "^1.6.0" } }, "sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g=="],
"undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
@@ -2694,6 +2695,8 @@
"@grpc/grpc-js/@grpc/proto-loader": ["@grpc/proto-loader@0.7.13", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw=="],
"@inquirer/core/@types/node": ["@types/node@20.14.6", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw=="],
"@inquirer/core/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
@@ -2790,10 +2793,28 @@
"@testing-library/react/react": ["react@file:../node_modules/react", {}],
"@types/cors/@types/node": ["@types/node@20.14.6", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw=="],
"@types/eslint/@types/estree": ["@types/estree@1.0.5", "", {}, "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="],
"@types/eslint-scope/@types/estree": ["@types/estree@1.0.5", "", {}, "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="],
"@types/is-buffer/@types/node": ["@types/node@20.14.6", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw=="],
"@types/mute-stream/@types/node": ["@types/node@20.14.6", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw=="],
"@types/oboe/@types/node": ["@types/node@20.14.6", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw=="],
"@types/superagent/@types/node": ["@types/node@20.14.6", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw=="],
"@types/utf-8-validate/@types/node": ["@types/node@20.14.6", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw=="],
"@types/whatwg-url/@types/node": ["@types/node@20.14.6", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw=="],
"@types/ws/@types/node": ["@types/node@20.14.6", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw=="],
"@types/yauzl/@types/node": ["@types/node@20.14.6", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw=="],
"@verdaccio/auth/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
"@verdaccio/commons-api/http-status-codes": ["http-status-codes@2.2.0", "", {}, "sha512-feERVo9iWxvnejp3SEfm/+oNG517npqL2/PIA8ORjyOZjGC7TwCRQsZylciLS64i6pJ0wRYz3rkXLRwbtFa8Ng=="],
@@ -2890,6 +2911,8 @@
"browserify-zlib/pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="],
"bun-types/@types/node": ["@types/node@20.14.6", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw=="],
"cacache/fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="],
"cacache/lru-cache": ["lru-cache@10.2.2", "", {}, "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ=="],
@@ -2934,6 +2957,8 @@
"engine.io/@types/cookie": ["@types/cookie@0.4.1", "", {}, "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="],
"engine.io/@types/node": ["@types/node@20.14.6", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw=="],
"engine.io/cookie": ["cookie@0.4.2", "", {}, "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA=="],
"engine.io/debug": ["debug@4.3.5", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg=="],
@@ -3002,6 +3027,8 @@
"jest-diff/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="],
"jest-worker/@types/node": ["@types/node@20.14.6", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw=="],
"jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"jsdom/form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="],
@@ -3086,6 +3113,8 @@
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
"protobufjs/@types/node": ["@types/node@20.14.6", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw=="],
"proxy-agent/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
"proxy-agent/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
@@ -3164,6 +3193,8 @@
"strip-literal/acorn": ["acorn@8.12.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw=="],
"stripe/@types/node": ["@types/node@20.14.6", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw=="],
"superagent/debug": ["debug@4.3.5", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg=="],
"superagent/semver": ["semver@7.6.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w=="],
@@ -3188,6 +3219,8 @@
"tsyringe/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
"type-graphql/@types/node": ["@types/node@20.14.6", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw=="],
"type-graphql/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
"type-graphql/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@@ -3234,6 +3267,8 @@
"vite-node/vite": ["vite@4.5.3", "", { "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", "rollup": "^3.27.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@types/node": ">= 14", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg=="],
"vitest/@types/node": ["@types/node@20.14.6", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw=="],
"vitest/acorn": ["acorn@8.12.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw=="],
"vitest/debug": ["debug@4.3.5", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg=="],
@@ -3268,6 +3303,8 @@
"@cypress/request/tough-cookie/universalify": ["universalify@0.2.0", "", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="],
"@inquirer/core/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"@inquirer/core/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"@inquirer/core/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
@@ -3298,6 +3335,24 @@
"@testing-library/dom/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"@types/cors/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"@types/is-buffer/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"@types/mute-stream/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"@types/oboe/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"@types/superagent/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"@types/utf-8-validate/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"@types/whatwg-url/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"@types/ws/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"@types/yauzl/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"@verdaccio/config/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"@verdaccio/local-storage-legacy/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="],
@@ -3432,6 +3487,8 @@
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"bun-types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"cli-highlight/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
@@ -3464,6 +3521,8 @@
"engine.io-client/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="],
"engine.io/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"engine.io/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="],
"express/body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
@@ -3492,6 +3551,8 @@
"jest-diff/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
"jest-worker/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"jest-worker/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"jsdom/https-proxy-agent/agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="],
@@ -3512,6 +3573,8 @@
"peek-stream/duplexify/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"protobufjs/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"proxy-agent/socks-proxy-agent/debug": ["debug@4.3.5", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg=="],
"pumpify/duplexify/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
@@ -3542,6 +3605,8 @@
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"stripe/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"superagent/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="],
"table/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
@@ -3554,10 +3619,14 @@
"through2/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"type-graphql/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"typeorm/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"unstorage/chokidar/readdirp": ["readdirp@4.0.2", "", {}, "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA=="],
"v8-heapsnapshot/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"verdaccio-audit/express/body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="],
"verdaccio-audit/express/cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
@@ -3678,6 +3747,8 @@
"vite/rollup/@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.37.0", "", { "os": "win32", "cpu": "x64" }, "sha512-LWbXUBwn/bcLx2sSsqy7pK5o+Nr+VCoRoAohfJ5C/aBio9nfJmGQqHAhU6pwxV/RmyTk5AqdySma7uwWGlmeuA=="],
"vitest/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"vitest/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="],
"vitest/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.4.15", "", {}, "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="],

View File

@@ -1,25 +0,0 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const { Worker } = require('worker_threads');
// Like test-async-hooks-worker-promise.js but doing a trivial counter increase
// after process.exit(). This should not make a difference, but apparently it
// does. This is *also* different from test-async-hooks-worker-promise-3.js,
// in that the statement is an ArrayBuffer access rather than a full method,
// which *also* makes a difference even though it shouldnt.
const workerData = new Int32Array(new SharedArrayBuffer(4));
const w = new Worker(`
const { createHook } = require('async_hooks');
const { workerData } = require('worker_threads');
setImmediate(async () => {
createHook({ init() {} }).enable();
await 0;
process.exit();
workerData[0]++;
});
`, { eval: true, workerData });
w.on('exit', common.mustCall(() => assert.strictEqual(workerData[0], 0)));

View File

@@ -0,0 +1,40 @@
'use strict';
require('../common');
// This test ensures that the messages from the internal
// message port are drained before the call to 'kDispose',
// and so all the stdio messages from the worker are processed
// in the parent and are pushed to their target streams.
const assert = require('assert');
const {
Worker,
isMainThread,
parentPort,
threadId,
} = require('worker_threads');
if (isMainThread) {
const workerIdsToOutput = new Map();
for (let i = 0; i < 2; i++) {
const worker = new Worker(__filename, { stdout: true });
const workerOutput = [];
workerIdsToOutput.set(worker.threadId, workerOutput);
worker.on('message', console.log);
worker.stdout.on('data', (chunk) => {
workerOutput.push(chunk.toString().trim());
});
}
process.on('exit', () => {
for (const [threadId, workerOutput] of workerIdsToOutput) {
assert.ok(workerOutput.includes(`1 threadId: ${threadId}`));
assert.ok(workerOutput.includes(`2 threadId: ${threadId}`));
}
});
} else {
console.log(`1 threadId: ${threadId}`);
console.log(`2 threadId: ${threadId}`);
parentPort.postMessage(Array(100).fill(1));
}

View File

@@ -0,0 +1,20 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const { Worker, isMainThread } = require('worker_threads');
// Regression test for https://github.com/nodejs/node/issues/28144.
if (isMainThread) {
const w = new Worker(__filename);
w.on('exit', common.mustCall((status) => {
assert.strictEqual(status, 0);
}));
w.stdout.on('data', common.mustCall(10));
} else {
process.stdin.on('data', () => {});
for (let i = 0; i < 10; ++i) {
process.stdout.write(`processing(${i})\n`, common.mustSucceed());
}
}

View File

@@ -0,0 +1,24 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const { Worker, isMainThread } = require('worker_threads');
if (isMainThread) {
const w = new Worker(__filename, { stdout: true });
const expected = 'hello world';
let data = '';
w.stdout.setEncoding('utf8');
w.stdout.on('data', (chunk) => {
data += chunk;
});
w.on('exit', common.mustCall(() => {
assert.strictEqual(data, expected);
}));
} else {
process.stdout.write('hello');
process.stdout.write(' ');
process.stdout.write('world');
process.exit(0);
}

View File

@@ -0,0 +1,25 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const { Worker, isMainThread } = require('worker_threads');
if (isMainThread) {
const w = new Worker(__filename, { stdout: true });
const expected = 'hello world';
let data = '';
w.stdout.setEncoding('utf8');
w.stdout.on('data', (chunk) => {
data += chunk;
});
w.on('exit', common.mustCall(() => {
assert.strictEqual(data, expected);
}));
} else {
process.on('exit', () => {
process.stdout.write(' ');
process.stdout.write('world');
});
process.stdout.write('hello');
}

View File

@@ -0,0 +1,42 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const fs = require('fs');
const util = require('util');
const { Writable } = require('stream');
const { Worker, isMainThread } = require('worker_threads');
class BufferingWritable extends Writable {
constructor() {
super();
this.chunks = [];
}
_write(chunk, enc, cb) {
this.chunks.push(chunk);
cb();
}
get buffer() {
return Buffer.concat(this.chunks);
}
}
if (isMainThread) {
const original = new BufferingWritable();
const passed = new BufferingWritable();
const w = new Worker(__filename, { stdin: true, stdout: true });
const source = fs.createReadStream(process.execPath, { end: 1_000_000 });
source.pipe(w.stdin);
source.pipe(original);
w.stdout.pipe(passed);
passed.on('finish', common.mustCall(() => {
assert.strictEqual(original.buffer.compare(passed.buffer), 0,
`Original: ${util.inspect(original.buffer)}, ` +
`Actual: ${util.inspect(passed.buffer)}`);
}));
} else {
process.stdin.pipe(process.stdout);
}

View File

@@ -1,8 +1,9 @@
import { bunEnv, bunExe } from "harness";
import { describe, expect, it, mock, test } from "bun:test";
import { bunEnv, bunExe, isDebug } from "harness";
import { once } from "node:events";
import fs from "node:fs";
import { join, relative, resolve } from "node:path";
import { Readable } from "node:stream";
import { duplexPair, Readable, Writable } from "node:stream";
import wt, {
BroadcastChannel,
getEnvironmentData,
@@ -108,8 +109,10 @@ test("all worker_threads worker instance properties are present", async () => {
expect(worker.ref).toBeFunction();
expect(worker.unref).toBeFunction();
expect(worker.stdin).toBeNull();
expect(worker.stdout).toBeNull();
expect(worker.stderr).toBeNull();
expect(worker.stdout).toBeInstanceOf(Readable);
expect(worker.stderr).toBeInstanceOf(Readable);
expect(Object.getOwnPropertyDescriptor(Worker.prototype, "stdout")?.get).toBeFunction();
expect(Object.getOwnPropertyDescriptor(Worker.prototype, "stderr")?.get).toBeFunction();
expect(worker.performance).toBeDefined();
expect(worker.terminate).toBeFunction();
expect(worker.postMessage).toBeFunction();
@@ -280,7 +283,8 @@ describe("execArgv option", async () => {
// TODO(@190n) get our handling of non-string array elements in line with Node's
});
test("eval does not leak source code", async () => {
// debug builds use way more memory and do not give useful results for this test
test.skipIf(isDebug)("eval does not leak source code", async () => {
const proc = Bun.spawn({
cmd: [bunExe(), "eval-source-leak-fixture.js"],
env: bunEnv,
@@ -423,6 +427,345 @@ describe("error event", () => {
});
});
describe("stdio", () => {
type OutputStream = "stdout" | "stderr";
function readToEnd(stream: Readable): Promise<string> {
let data = "";
const { promise, resolve, reject } = Promise.withResolvers<string>();
stream.on("error", reject);
stream.on("data", chunk => {
expect(chunk).toBeInstanceOf(Buffer);
data += chunk.toString("utf8");
});
stream.on("end", () => resolve(data));
return promise;
}
function overrideProcessStdio<S extends OutputStream>(which: S, stream: Writable): Disposable {
const originalStream = process[which];
// stream is missing the `fd` property that the real process streams have, but we
// don't need it
// @ts-expect-error
process[which] = stream;
return {
[Symbol.dispose]() {
process[which] = originalStream;
},
};
}
function captureProcessStdio<S extends OutputStream>(
stream: S,
): Disposable & { data: Promise<string>; end: () => void } {
const [streamToInstall, streamToObserve] = duplexPair();
return {
...overrideProcessStdio(stream, streamToInstall),
data: readToEnd(streamToObserve),
end: () => streamToInstall.end(),
};
}
describe.each<OutputStream>(["stdout", "stderr"])("%s", stream => {
it(`process.${stream} written in worker writes to parent process.${stream}`, async () => {
using capture = captureProcessStdio(stream);
const worker = new Worker(
String.raw/* js */ `
import assert from "node:assert";
process.${stream}.write("hello", (err) => {
assert.strictEqual(err, null);
process.${stream}.write("\ncallback 1");
});
// " world"
process.${stream}.write(new Uint16Array([0x7720, 0x726f, 0x646c]), (err) => {
assert.strictEqual(err, null);
process.${stream}.write("\ncallback 2");
});
`,
{ eval: true },
);
const [code] = await once(worker, "exit");
expect(code).toBe(0);
capture.end();
expect(await capture.data).toBe("hello world\ncallback 1\ncallback 2");
});
it(`process.${stream} written in worker writes to worker.${stream} in parent`, async () => {
const worker = new Worker(`process.${stream}.write("hello");`, { eval: true });
const resultPromise = readToEnd(worker[stream]);
const [code] = await once(worker, "exit");
expect(code).toBe(0);
expect(await resultPromise).toBe("hello");
});
it(`can still receive data on worker.${stream} if you override it later`, async () => {
const worker = new Worker(`process.${stream}.write("hello");`, { eval: true });
const resultPromise = readToEnd(worker[stream]);
Object.defineProperty(worker, stream, { value: undefined });
const [code] = await once(worker, "exit");
expect(code).toBe(0);
expect(await resultPromise).toBe("hello");
});
const consoleFunction = stream == "stdout" ? "log" : "error";
it(`console.${consoleFunction} in worker writes to both streams in parent`, async () => {
using capture = captureProcessStdio(stream);
const worker = new Worker(`console.${consoleFunction}("hello");`, { eval: true });
const resultPromise = readToEnd(worker[stream]);
const [code] = await once(worker, "exit");
expect(code).toBe(0);
capture.end();
expect(await capture.data).toBe("hello\n");
expect(await resultPromise).toBe("hello\n");
});
describe(`with ${stream}: true option`, () => {
it(`writes to worker.${stream} but not process.${stream}`, async () => {
using capture = captureProcessStdio(stream);
const worker = new Worker(`process.${stream}.write("hello");`, { eval: true, [stream]: true });
const resultPromise = readToEnd(worker[stream]);
const [code] = await once(worker, "exit");
expect(code).toBe(0);
capture.end();
expect(await capture.data).toBe("");
expect(await resultPromise).toBe("hello");
});
});
it("worker write() doesn't wait for parent _write() to complete", async () => {
const sharedBuffer = new SharedArrayBuffer(4);
const sharedArray = new Int32Array(sharedBuffer);
const { promise, resolve } = Promise.withResolvers();
const writeFn = mock((chunk: Buffer, encoding: string, callback: () => void) => {
expect(chunk).toEqual(Buffer.from("hello"));
expect(encoding).toBe("buffer");
// wait for worker to indicate that its write() callback ran
Atomics.wait(sharedArray, 0, 0);
// now run the callback
callback();
// and resolve our promise
resolve();
});
class DelayStream extends Writable {
_write(data: Buffer, encoding: string, callback: () => void) {
return writeFn(data, encoding, callback);
}
}
using override = overrideProcessStdio(stream, new DelayStream());
const worker = new Worker(
/* js */ `
import { workerData } from "node:worker_threads";
const sharedArray = new Int32Array(workerData);
import assert from "node:assert";
process.${stream}.write("hello", "utf8", (err) => {
assert.strictEqual(err, null);
// tell parent that our callback has run
Atomics.store(sharedArray, 0, 1);
Atomics.notify(sharedArray, 0, 1);
});
`,
{ eval: true, workerData: sharedBuffer },
);
const [code] = await once(worker, "exit");
expect(code).toBe(0);
await promise;
expect(writeFn).toHaveBeenCalledTimes(1);
});
it(`console uses overridden process.${stream} in worker`, async () => {
const worker = new Worker(
/* js */ `
import { Writable } from "node:stream";
const original = process.${stream};
class WrapStream extends Writable {
_write(chunk, encoding, callback) {
original.write("[wrapped] " + chunk.toString());
callback();
}
}
process.${stream} = new WrapStream();
console.${consoleFunction}("hello");
`,
{ eval: true, [stream]: true },
);
const resultPromise = readToEnd(worker[stream]);
const [code] = await once(worker, "exit");
expect(code).toBe(0);
expect(await resultPromise).toBe("[wrapped] hello\n");
});
it("has no fd", async () => {
const worker = new Worker(
/* js */ `
import assert from "node:assert";
assert.strictEqual(process.${stream}.fd, undefined);
`,
{ eval: true },
);
const [code] = await once(worker, "exit");
expect(code).toBe(0);
});
});
describe("console", () => {
it("all functions are captured", async () => {
const worker = new Worker(
/* js */ `
console.assert();
console.assert(false);
// TODO: https://github.com/oven-sh/bun/issues/19953
// this should be "Assertion failed: should be true," not "should be true"
// but we still want to make sure it is captured in workers
console.assert(false, "should be true");
console.debug("debug");
console.error("error");
console.info("info");
console.log("log");
console.table([{ a: 5 }]);
// TODO: https://github.com/oven-sh/bun/issues/19952
// this goes to the wrong place but we still want to make sure it is captured in workers
console.trace("trace");
console.warn("warn");
`,
{ eval: true, stdout: true, stderr: true },
);
// normalize the random blob URL and lines and columns from internal modules
const stdout = (await readToEnd(worker.stdout))
.replace(/blob:[0-9a-f\-]{36}/, "blob:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
.replaceAll(/\(\d+:\d+\)$/gm, "(line:col)");
const stderr = await readToEnd(worker.stderr);
let expectedStdout = `debug
info
log
┌───┬───┐
│ │ a │
├───┼───┤
│ 0 │ 5 │
└───┴───┘
trace
at blob:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:15:17
at loadAndEvaluateModule (line:col)
`;
if (isDebug) {
expectedStdout += ` at asyncFunctionResume (line:col)
at promiseReactionJobWithoutPromiseUnwrapAsyncContext (line:col)
at promiseReactionJob (line:col)
`;
}
expect(stdout).toBe(expectedStdout);
expect(stderr).toBe(`Assertion failed
Assertion failed
should be true
error
warn
`);
});
it("handles exceptions", async () => {
const cases = [
{
code: /* js */ `process.stdout.write = () => { throw new Error("write()"); }; console.log("hello");`,
expectedException: {
name: "Error",
message: "write()",
},
},
{
code: /* js */ `process.stdout.write = 6; console.log("hello");`,
expectedException: {
name: "TypeError",
message: expect.stringMatching(/is not a function.*is 6/),
},
},
{
code: /* js */ `Object.defineProperty(process.stdout, "write", { get() { throw new Error("write getter"); } }); console.log("hello");`,
expectedException: {
name: "Error",
message: "write getter",
},
},
{
code: /* js */ `Object.defineProperty(process, "stdout", { get() { throw new Error("stdout getter"); } }); console.log("hello");`,
expectedException: {
name: "Error",
message: "stdout getter",
},
},
];
for (const { code, expectedException } of cases) {
const worker = new Worker(code, { eval: true, stdout: true });
const stdoutPromise = readToEnd(worker.stdout);
const [exception] = await once(worker, "error");
expect(exception).toMatchObject(expectedException);
expect(await stdoutPromise).toBe("");
}
});
// blocked on more JS internals that use `process` while it has been overridden
it.todo("works if the entire process object is overridden", async () => {
const worker = new Worker(/* js */ `process = 5; console.log("hello");`, { eval: true, stdout: true });
expect(await readToEnd(worker.stdout)).toBe("hello\n");
});
});
describe("stdin", () => {
it("by default, process.stdin is readable and worker.stdin is null", async () => {
const worker = new Worker(
/* js */ `
import assert from "node:assert";
assert.strictEqual(process.stdin.constructor.name, "ReadableWorkerStdio");
`,
{ eval: true },
);
expect(worker.stdin).toBeNull();
const [code] = await once(worker, "exit");
expect(code).toBe(0);
});
it("has no fd", async () => {
const worker = new Worker(
/* js */ `
import assert from "node:assert";
assert.strictEqual(process.stdin.fd, undefined);
`,
{ eval: true },
);
const [code] = await once(worker, "exit");
expect(code).toBe(0);
});
it.todo("does not keep the event loop alive if worker does not listen for events", async () => {});
it.todo("hangs if parent does not call end()", async () => {});
it("child can read data from parent", async () => {
const chunks: Buffer[] = [];
const { promise, resolve, reject } = Promise.withResolvers();
const worker = new Worker("process.stdin.pipe(process.stdout)", { stdin: true, stdout: true, eval: true });
expect(worker.stdin!.constructor.name).toBe("WritableWorkerStdio");
worker.on("error", reject);
worker.stdout.on("data", chunk => {
chunks.push(chunk);
if (chunks.length == 2) resolve();
if (chunks.length > 2) throw new Error("too much data");
});
worker.stdin!.write("hello");
// " world"
worker.stdin!.write(new Uint16Array([0x7720, 0x726f, 0x646c]));
await promise;
expect(chunks).toEqual([Buffer.from("hello"), Buffer.from(" world")]);
worker.stdin!.end();
});
});
});
describe("getHeapSnapshot", () => {
test("throws if the wrong options are passed", () => {
const worker = new Worker("", { eval: true });

View File

@@ -0,0 +1,41 @@
import { Writable } from "node:stream";
class WrapStream extends Writable {
#base;
#message;
constructor(base, message) {
super();
this.#base = base;
this.#message = message;
}
_write(chunk, encoding, callback) {
const string = chunk.toString("utf8");
this.#base.write(`[${this.#message}] ${string}`, "utf8", callback);
}
}
if (Bun.isMainThread) {
process.stdout = new WrapStream(process.stdout, "parent process.stdout");
process.stderr = new WrapStream(process.stderr, "parent process.stderr");
new Worker(import.meta.filename);
} else {
process.stdout = new WrapStream(process.stdout, "worker process.stdout");
process.stderr = new WrapStream(process.stderr, "worker process.stderr");
console.assert();
console.assert(false);
// TODO: https://github.com/oven-sh/bun/issues/19953
// this should be "Assertion failed: should be true," not "should be true"
// but we still want to make sure it is not in workers
console.assert(false, "should be true");
console.debug("debug");
console.error("error");
console.info("info");
console.log("log");
console.table([{ a: 5 }]);
// TODO: https://github.com/oven-sh/bun/issues/19952
// this goes to the wrong place but we still want to make sure it is not in workers
console.trace("trace");
console.warn("warn");
}

View File

@@ -0,0 +1,26 @@
import { Writable } from "node:stream";
class WrapStream extends Writable {
#base;
#message;
constructor(base, message) {
super();
this.#base = base;
this.#message = message;
}
_write(chunk, encoding, callback) {
const string = chunk.toString("utf8");
this.#base.write(`[${this.#message}] ${string}`, "utf8", callback);
}
}
if (Bun.isMainThread) {
process.stdout = new WrapStream(process.stdout, "parent process.stdout");
process.stderr = new WrapStream(process.stderr, "parent process.stderr");
new Worker(import.meta.filename);
} else {
process.stdout.write("stdout");
process.stderr.write("stderr");
}

View File

@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test";
import { once } from "events";
import { bunEnv, bunExe } from "harness";
import { bunEnv, bunExe, isDebug } from "harness";
import path from "path";
import wt from "worker_threads";
@@ -296,6 +296,66 @@ describe("web worker", () => {
expect(err.error).toBe(null);
});
});
describe("stdio", () => {
test("process stdio in worker does not go to process stdio in parent", async () => {
const proc = Bun.spawn({
cmd: [bunExe(), "worker-fixture-process-stdio.js"],
env: bunEnv,
cwd: __dirname,
stdout: "pipe",
stderr: "pipe",
});
await proc.exited;
expect(proc.exitCode).toBe(0);
const stdout = await new Response(proc.stdout).text();
const stderr = await new Response(proc.stderr).text();
expect(stdout).toBe("stdout");
expect(stderr).toBe("stderr");
});
test("console functions in worker do not go to process stdio in worker or parent", async () => {
const proc = Bun.spawn({
cmd: [bunExe(), "worker-fixture-console.js"],
env: bunEnv,
cwd: __dirname,
stdout: "pipe",
stderr: "pipe",
});
await proc.exited;
expect(proc.exitCode).toBe(0);
// normalize lines and columns from internal modules
const stdout = (await new Response(proc.stdout).text()).replaceAll(/\(\d+:\d+\)$/gm, "(line:col)");
const stderr = await new Response(proc.stderr).text();
let expectedStdout = `debug
info
log
┌───┬───┐
│ │ a │
├───┼───┤
│ 0 │ 5 │
└───┴───┘
trace
at ${__dirname}${path.sep}worker-fixture-console.js:39:11
at loadAndEvaluateModule (line:col)
`;
if (isDebug) {
expectedStdout += ` at asyncFunctionResume (line:col)
at promiseReactionJobWithoutPromiseUnwrapAsyncContext (line:col)
at promiseReactionJob (line:col)
`;
}
expect(stdout).toBe(expectedStdout);
expect(stderr).toBe(`Assertion failed
Assertion failed
should be true
error
warn
`);
});
});
});
// TODO: move to node:worker_threads tests directory

View File

@@ -6,7 +6,8 @@
"@types/supertest": "2.0.12",
"@types/utf-8-validate": "5.0.0",
"@types/ws": "8.5.10",
"@types/puppeteer": "7.0.4"
"@types/puppeteer": "7.0.4",
"@types/node": "22.6.0"
},
"dependencies": {
"@astrojs/node": "9.1.3",