mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 14:22:01 +00:00
Compare commits
5 Commits
riskymh/np
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e87c40cb7d | ||
|
|
4250ce6157 | ||
|
|
f8dce87f24 | ||
|
|
359f04d81f | ||
|
|
9ce2504554 |
8
packages/bun-types/bun.d.ts
vendored
8
packages/bun-types/bun.d.ts
vendored
@@ -5791,11 +5791,11 @@ declare module "bun" {
|
||||
* @category Process Management
|
||||
*
|
||||
* ```js
|
||||
* const subprocess = Bun.spawn({
|
||||
* const proc = Bun.spawn({
|
||||
* cmd: ["echo", "hello"],
|
||||
* stdout: "pipe",
|
||||
* });
|
||||
* const text = await readableStreamToText(subprocess.stdout);
|
||||
* const text = await proc.stdout.text();
|
||||
* console.log(text); // "hello\n"
|
||||
* ```
|
||||
*
|
||||
@@ -5829,8 +5829,8 @@ declare module "bun" {
|
||||
* Spawn a new process
|
||||
*
|
||||
* ```js
|
||||
* const {stdout} = Bun.spawn(["echo", "hello"]);
|
||||
* const text = await readableStreamToText(stdout);
|
||||
* const proc = Bun.spawn(["echo", "hello"]);
|
||||
* const text = await proc.stdout.text();
|
||||
* console.log(text); // "hello\n"
|
||||
* ```
|
||||
*
|
||||
|
||||
2
packages/bun-types/test.d.ts
vendored
2
packages/bun-types/test.d.ts
vendored
@@ -172,7 +172,7 @@ declare module "bun:test" {
|
||||
/**
|
||||
* Mock a module
|
||||
*/
|
||||
module: typeof mock.module;
|
||||
mock: typeof mock.module;
|
||||
/**
|
||||
* Restore all mocks to their original implementation
|
||||
*/
|
||||
|
||||
@@ -193,6 +193,9 @@ pub fn upgrade(this: *NodeHTTPResponse, data_value: JSValue, sec_websocket_proto
|
||||
if (this.raw_response) |raw_response| {
|
||||
this.raw_response = null;
|
||||
this.flags.upgraded = true;
|
||||
// Unref the poll_ref since the socket is now upgraded to WebSocket
|
||||
// and will have its own lifecycle management
|
||||
this.poll_ref.unref(this.server.globalThis().bunVM());
|
||||
_ = raw_response.upgrade(*ServerWebSocket, ws, websocket_key, sec_websocket_protocol_value, sec_websocket_extensions_value, upgrade_ctx);
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -416,8 +416,7 @@ extern "C" napi_status napi_set_property(napi_env env, napi_value target,
|
||||
|
||||
JSValue jsValue = toJS(value);
|
||||
|
||||
// Ignoring the return value matches JS sloppy mode
|
||||
(void)object->methodTable()->put(object, globalObject, identifier, jsValue, slot);
|
||||
(void)object->putInline(globalObject, identifier, jsValue, slot);
|
||||
NAPI_RETURN_SUCCESS_UNLESS_EXCEPTION(env);
|
||||
}
|
||||
|
||||
@@ -432,12 +431,12 @@ extern "C" napi_status napi_set_element(napi_env env, napi_value object_,
|
||||
JSValue value = toJS(value_);
|
||||
NAPI_RETURN_EARLY_IF_FALSE(env, !object.isEmpty() && !value.isEmpty(), napi_invalid_arg);
|
||||
|
||||
auto globalObject = toJS(env);
|
||||
JSObject* jsObject = object.getObject();
|
||||
NAPI_RETURN_EARLY_IF_FALSE(env, jsObject, napi_array_expected);
|
||||
|
||||
jsObject->methodTable()->putByIndex(jsObject, toJS(env), index, value, false);
|
||||
NAPI_RETURN_IF_EXCEPTION(env);
|
||||
NAPI_RETURN_SUCCESS(env);
|
||||
(void)jsObject->putByIndexInline(globalObject, index, value, false);
|
||||
NAPI_RETURN_SUCCESS_UNLESS_EXCEPTION(env);
|
||||
}
|
||||
|
||||
extern "C" napi_status napi_has_element(napi_env env, napi_value object_,
|
||||
@@ -454,9 +453,9 @@ extern "C" napi_status napi_has_element(napi_env env, napi_value object_,
|
||||
NAPI_RETURN_EARLY_IF_FALSE(env, jsObject, napi_array_expected);
|
||||
|
||||
bool has_property = jsObject->hasProperty(toJS(env), index);
|
||||
NAPI_RETURN_IF_EXCEPTION(env);
|
||||
*result = has_property;
|
||||
|
||||
NAPI_RETURN_SUCCESS_UNLESS_EXCEPTION(env);
|
||||
NAPI_RETURN_SUCCESS(env);
|
||||
}
|
||||
|
||||
extern "C" napi_status napi_has_property(napi_env env, napi_value object,
|
||||
@@ -472,8 +471,13 @@ extern "C" napi_status napi_has_property(napi_env env, napi_value object,
|
||||
NAPI_RETURN_IF_EXCEPTION(env);
|
||||
|
||||
auto keyProp = toJS(key);
|
||||
*result = target->hasProperty(globalObject, keyProp.toPropertyKey(globalObject));
|
||||
NAPI_RETURN_SUCCESS_UNLESS_EXCEPTION(env);
|
||||
JSC::PropertyName name = keyProp.toPropertyKey(globalObject);
|
||||
NAPI_RETURN_IF_EXCEPTION(env);
|
||||
|
||||
bool hasProperty = target->hasProperty(globalObject, name);
|
||||
NAPI_RETURN_IF_EXCEPTION(env);
|
||||
*result = hasProperty;
|
||||
NAPI_RETURN_SUCCESS(env);
|
||||
}
|
||||
|
||||
extern "C" napi_status napi_get_date_value(napi_env env, napi_value value, double* result)
|
||||
@@ -507,8 +511,26 @@ extern "C" napi_status napi_get_property(napi_env env, napi_value object,
|
||||
|
||||
auto keyProp = toJS(key);
|
||||
JSC::EnsureStillAliveScope ensureAlive2(keyProp);
|
||||
*result = toNapi(target->get(globalObject, keyProp.toPropertyKey(globalObject)), globalObject);
|
||||
NAPI_RETURN_SUCCESS_UNLESS_EXCEPTION(env);
|
||||
PropertySlot slot(target, PropertySlot::InternalMethodType::Get);
|
||||
auto propertyName = keyProp.toPropertyKey(globalObject);
|
||||
NAPI_RETURN_IF_EXCEPTION(env);
|
||||
|
||||
const auto index = parseIndex(propertyName);
|
||||
|
||||
bool hasProperty = index ? target->getPropertySlot(globalObject, *index, slot)
|
||||
: target->getNonIndexPropertySlot(globalObject, propertyName, slot);
|
||||
|
||||
NAPI_RETURN_IF_EXCEPTION(env);
|
||||
|
||||
if (!hasProperty) {
|
||||
*result = toNapi(jsUndefined(), globalObject);
|
||||
} else {
|
||||
JSValue resultValue = slot.getValue(globalObject, propertyName);
|
||||
NAPI_RETURN_IF_EXCEPTION(env);
|
||||
*result = toNapi(resultValue, globalObject);
|
||||
}
|
||||
|
||||
NAPI_RETURN_SUCCESS(env);
|
||||
}
|
||||
|
||||
extern "C" napi_status napi_delete_property(napi_env env, napi_value object,
|
||||
@@ -524,7 +546,11 @@ extern "C" napi_status napi_delete_property(napi_env env, napi_value object,
|
||||
NAPI_RETURN_IF_EXCEPTION(env);
|
||||
|
||||
auto keyProp = toJS(key);
|
||||
auto deleteResult = target->deleteProperty(globalObject, keyProp.toPropertyKey(globalObject));
|
||||
auto name = JSC::PropertyName(keyProp.toPropertyKey(globalObject));
|
||||
NAPI_RETURN_IF_EXCEPTION(env);
|
||||
|
||||
auto deleteResult = target->deleteProperty(globalObject, name);
|
||||
|
||||
NAPI_RETURN_IF_EXCEPTION(env);
|
||||
|
||||
if (result) [[likely]] {
|
||||
@@ -550,8 +576,13 @@ extern "C" napi_status napi_has_own_property(napi_env env, napi_value object,
|
||||
JSValue keyProp = toJS(key);
|
||||
NAPI_RETURN_EARLY_IF_FALSE(env, keyProp.isString() || keyProp.isSymbol(), napi_name_expected);
|
||||
|
||||
*result = target->hasOwnProperty(globalObject, JSC::PropertyName(keyProp.toPropertyKey(globalObject)));
|
||||
NAPI_RETURN_SUCCESS_UNLESS_EXCEPTION(env);
|
||||
auto name = JSC::PropertyName(keyProp.toPropertyKey(globalObject));
|
||||
NAPI_RETURN_IF_EXCEPTION(env);
|
||||
|
||||
bool hasOwnProperty = target->hasOwnProperty(globalObject, name);
|
||||
NAPI_RETURN_IF_EXCEPTION(env);
|
||||
*result = hasOwnProperty;
|
||||
NAPI_RETURN_SUCCESS(env);
|
||||
}
|
||||
|
||||
extern "C" napi_status napi_set_named_property(napi_env env, napi_value object,
|
||||
@@ -575,11 +606,10 @@ extern "C" napi_status napi_set_named_property(napi_env env, napi_value object,
|
||||
JSC::EnsureStillAliveScope ensureAlive2(target);
|
||||
|
||||
auto nameStr = WTF::String::fromUTF8({ utf8name, strlen(utf8name) });
|
||||
auto identifier = JSC::Identifier::fromString(vm, WTFMove(nameStr));
|
||||
|
||||
auto name = JSC::PropertyName(JSC::Identifier::fromString(vm, WTFMove(nameStr)));
|
||||
PutPropertySlot slot(target, false);
|
||||
|
||||
target->methodTable()->put(target, globalObject, identifier, jsValue, slot);
|
||||
target->putInline(globalObject, name, jsValue, slot);
|
||||
NAPI_RETURN_SUCCESS_UNLESS_EXCEPTION(env);
|
||||
}
|
||||
|
||||
|
||||
@@ -633,6 +633,25 @@ pub fn unrefConcurrently(this: *EventLoop) void {
|
||||
this.wakeup();
|
||||
}
|
||||
|
||||
/// Testing API to expose event loop state
|
||||
pub fn getActiveTasks(globalObject: *jsc.JSGlobalObject, _: *jsc.CallFrame) bun.JSError!jsc.JSValue {
|
||||
const vm = globalObject.bunVM();
|
||||
const event_loop = vm.event_loop;
|
||||
|
||||
const result = jsc.JSValue.createEmptyObject(globalObject, 3);
|
||||
result.put(globalObject, jsc.ZigString.static("activeTasks"), jsc.JSValue.jsNumber(vm.active_tasks));
|
||||
result.put(globalObject, jsc.ZigString.static("concurrentRef"), jsc.JSValue.jsNumber(event_loop.concurrent_ref.load(.seq_cst)));
|
||||
|
||||
// Get num_polls from uws loop (POSIX) or active_handles from libuv (Windows)
|
||||
const num_polls: i32 = if (Environment.isWindows)
|
||||
@intCast(bun.windows.libuv.Loop.get().active_handles)
|
||||
else
|
||||
uws.Loop.get().num_polls;
|
||||
result.put(globalObject, jsc.ZigString.static("numPolls"), jsc.JSValue.jsNumber(num_polls));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
pub const AnyEventLoop = @import("./event_loop/AnyEventLoop.zig").AnyEventLoop;
|
||||
pub const ConcurrentPromiseTask = @import("./event_loop/ConcurrentPromiseTask.zig").ConcurrentPromiseTask;
|
||||
pub const WorkTask = @import("./event_loop/WorkTask.zig").WorkTask;
|
||||
|
||||
@@ -5,38 +5,62 @@ const PathToSourceIndexMap = @This();
|
||||
/// We assume it's arena allocated.
|
||||
map: Map = .{},
|
||||
|
||||
const Map = bun.StringHashMapUnmanaged(Index.Int);
|
||||
/// HashMap context that makes path lookups namespace-aware.
|
||||
/// For file namespace, uses only path.text for backwards compatibility.
|
||||
/// For other namespaces, combines namespace and path in the hash.
|
||||
const PathHashContext = struct {
|
||||
pub fn hash(_: @This(), path: Fs.Path) u64 {
|
||||
return path.hashKey();
|
||||
}
|
||||
|
||||
pub fn eql(_: @This(), a: Fs.Path, b: Fs.Path) bool {
|
||||
// For file namespace, only compare path text
|
||||
if (a.isFile() and b.isFile()) {
|
||||
return bun.strings.eqlLong(a.text, b.text, true);
|
||||
}
|
||||
|
||||
// For non-file namespaces, compare both namespace and path
|
||||
return bun.strings.eqlLong(a.namespace, b.namespace, true) and
|
||||
bun.strings.eqlLong(a.text, b.text, true);
|
||||
}
|
||||
};
|
||||
|
||||
const Map = std.HashMapUnmanaged(Fs.Path, Index.Int, PathHashContext, std.hash_map.default_max_load_percentage);
|
||||
|
||||
pub fn getPath(this: *const PathToSourceIndexMap, path: *const Fs.Path) ?Index.Int {
|
||||
return this.get(path.text);
|
||||
return this.map.get(path.*);
|
||||
}
|
||||
|
||||
pub fn get(this: *const PathToSourceIndexMap, text: []const u8) ?Index.Int {
|
||||
return this.map.get(text);
|
||||
const file_path = Fs.Path.init(text);
|
||||
return this.map.get(file_path);
|
||||
}
|
||||
|
||||
pub fn putPath(this: *PathToSourceIndexMap, allocator: std.mem.Allocator, path: *const Fs.Path, value: Index.Int) bun.OOM!void {
|
||||
try this.map.put(allocator, path.text, value);
|
||||
try this.map.put(allocator, path.*, value);
|
||||
}
|
||||
|
||||
pub fn put(this: *PathToSourceIndexMap, allocator: std.mem.Allocator, text: []const u8, value: Index.Int) bun.OOM!void {
|
||||
try this.map.put(allocator, text, value);
|
||||
const file_path = Fs.Path.init(text);
|
||||
try this.map.put(allocator, file_path, value);
|
||||
}
|
||||
|
||||
pub fn getOrPutPath(this: *PathToSourceIndexMap, allocator: std.mem.Allocator, path: *const Fs.Path) bun.OOM!Map.GetOrPutResult {
|
||||
return this.getOrPut(allocator, path.text);
|
||||
return try this.map.getOrPut(allocator, path.*);
|
||||
}
|
||||
|
||||
pub fn getOrPut(this: *PathToSourceIndexMap, allocator: std.mem.Allocator, text: []const u8) bun.OOM!Map.GetOrPutResult {
|
||||
return try this.map.getOrPut(allocator, text);
|
||||
const file_path = Fs.Path.init(text);
|
||||
return try this.map.getOrPut(allocator, file_path);
|
||||
}
|
||||
|
||||
pub fn remove(this: *PathToSourceIndexMap, text: []const u8) bool {
|
||||
return this.map.remove(text);
|
||||
const file_path = Fs.Path.init(text);
|
||||
return this.map.remove(file_path);
|
||||
}
|
||||
|
||||
pub fn removePath(this: *PathToSourceIndexMap, path: *const Fs.Path) bool {
|
||||
return this.remove(path.text);
|
||||
return this.map.remove(path.*);
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
@@ -657,7 +657,7 @@ pub const BundleV2 = struct {
|
||||
const entry = bun.handleOom(this.pathToSourceIndexMap(target).getOrPut(this.allocator(), path.text));
|
||||
if (!entry.found_existing) {
|
||||
path.* = bun.handleOom(this.pathWithPrettyInitialized(path.*, target));
|
||||
entry.key_ptr.* = path.text;
|
||||
entry.key_ptr.* = path.*;
|
||||
const loader: Loader = brk: {
|
||||
const record: *ImportRecord = &this.graph.ast.items(.import_records)[import_record.importer_source_index].slice()[import_record.import_record_index];
|
||||
if (record.loader) |out_loader| {
|
||||
@@ -699,9 +699,9 @@ pub const BundleV2 = struct {
|
||||
.browser => .{ this.pathToSourceIndexMap(this.transpiler.options.target), this.pathToSourceIndexMap(.bake_server_components_ssr) },
|
||||
.bake_server_components_ssr => .{ this.pathToSourceIndexMap(this.transpiler.options.target), this.pathToSourceIndexMap(.browser) },
|
||||
};
|
||||
bun.handleOom(a.put(this.allocator(), entry.key_ptr.*, entry.value_ptr.*));
|
||||
bun.handleOom(a.putPath(this.allocator(), entry.key_ptr, entry.value_ptr.*));
|
||||
if (this.framework.?.server_components.?.separate_ssr_graph)
|
||||
bun.handleOom(b.put(this.allocator(), entry.key_ptr.*, entry.value_ptr.*));
|
||||
bun.handleOom(b.putPath(this.allocator(), entry.key_ptr, entry.value_ptr.*));
|
||||
}
|
||||
} else {
|
||||
out_source_index = Index.init(entry.value_ptr.*);
|
||||
@@ -736,7 +736,7 @@ pub const BundleV2 = struct {
|
||||
|
||||
path = bun.handleOom(this.pathWithPrettyInitialized(path, target));
|
||||
path.assertPrettyIsValid();
|
||||
entry.key_ptr.* = path.text;
|
||||
entry.key_ptr.* = path;
|
||||
entry.value_ptr.* = source_index.get();
|
||||
bun.handleOom(this.graph.ast.append(this.allocator(), JSAst.empty));
|
||||
|
||||
@@ -798,7 +798,7 @@ pub const BundleV2 = struct {
|
||||
|
||||
path.* = bun.handleOom(this.pathWithPrettyInitialized(path.*, target));
|
||||
path.assertPrettyIsValid();
|
||||
entry.key_ptr.* = path.text;
|
||||
entry.key_ptr.* = path.*;
|
||||
entry.value_ptr.* = source_index.get();
|
||||
bun.handleOom(this.graph.ast.append(this.allocator(), JSAst.empty));
|
||||
|
||||
@@ -2455,7 +2455,8 @@ pub const BundleV2 = struct {
|
||||
if (!existing.found_existing) {
|
||||
this.free_list.appendSlice(&.{ result.namespace, result.path }) catch {};
|
||||
path = bun.handleOom(this.pathWithPrettyInitialized(path, resolve.import_record.original_target));
|
||||
existing.key_ptr.* = path.text;
|
||||
// Update the key to use the arena-allocated path strings
|
||||
existing.key_ptr.* = path;
|
||||
|
||||
// We need to parse this
|
||||
const source_index = Index.init(@as(u32, @intCast(this.graph.ast.len)));
|
||||
@@ -3429,7 +3430,7 @@ pub const BundleV2 = struct {
|
||||
|
||||
const is_html_entrypoint = import_record_loader == .html and target.isServerSide() and this.transpiler.options.dev_server == null;
|
||||
|
||||
if (this.pathToSourceIndexMap(target).get(path.text)) |id| {
|
||||
if (this.pathToSourceIndexMap(target).getPath(path)) |id| {
|
||||
if (this.transpiler.options.dev_server != null and loader != .html) {
|
||||
import_record.path = this.graph.input_files.items(.source)[id].path;
|
||||
} else {
|
||||
@@ -3442,7 +3443,13 @@ pub const BundleV2 = struct {
|
||||
import_record.kind = .html_manifest;
|
||||
}
|
||||
|
||||
const resolve_entry = resolve_queue.getOrPut(path.text) catch |err| bun.handleOom(err);
|
||||
// Generate namespace-aware key for the resolve queue
|
||||
const resolve_key = if (path.isFile())
|
||||
path.text
|
||||
else
|
||||
std.fmt.allocPrint(this.allocator(), "{s}:::::{s}", .{ path.namespace, path.text }) catch |err| bun.handleOom(err);
|
||||
|
||||
const resolve_entry = resolve_queue.getOrPut(resolve_key) catch |err| bun.handleOom(err);
|
||||
if (resolve_entry.found_existing) {
|
||||
import_record.path = resolve_entry.value_ptr.*.path;
|
||||
continue;
|
||||
@@ -3451,7 +3458,11 @@ pub const BundleV2 = struct {
|
||||
path.* = bun.handleOom(this.pathWithPrettyInitialized(path.*, target));
|
||||
|
||||
import_record.path = path.*;
|
||||
resolve_entry.key_ptr.* = path.text;
|
||||
// Update key to use the arena-allocated path after pathWithPrettyInitialized
|
||||
resolve_entry.key_ptr.* = if (path.isFile())
|
||||
path.text
|
||||
else
|
||||
std.fmt.allocPrint(this.allocator(), "{s}:::::{s}", .{ path.namespace, path.text }) catch |err| bun.handleOom(err);
|
||||
debug("created ParseTask: {s}", .{path.text});
|
||||
const resolve_task = bun.handleOom(bun.default_allocator.create(ParseTask));
|
||||
resolve_task.* = ParseTask.init(&resolve_result, Index.invalid, this);
|
||||
|
||||
@@ -211,6 +211,9 @@ export const structuredCloneAdvanced: (
|
||||
|
||||
export const lsanDoLeakCheck = $newCppFunction("InternalForTesting.cpp", "jsFunction_lsanDoLeakCheck", 1);
|
||||
|
||||
export const getEventLoopStats: () => { activeTasks: number; concurrentRef: number; numPolls: number } =
|
||||
$newZigFunction("event_loop.zig", "getActiveTasks", 0);
|
||||
|
||||
export const hostedGitInfo = {
|
||||
parseUrl: $newZigFunction("hosted_git_info.zig", "TestingAPIs.jsParseUrl", 1),
|
||||
fromUrl: $newZigFunction("hosted_git_info.zig", "TestingAPIs.jsFromUrl", 1),
|
||||
|
||||
@@ -301,5 +301,73 @@ describe("bundler", () => {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Test that onLoad is called for different namespaces with same path
|
||||
itBundled("plugin/NamespaceOnLoadCalled", ({ root }) => {
|
||||
const callOrder: string[] = [];
|
||||
|
||||
return {
|
||||
files: {
|
||||
"index.ts": /* ts */ `
|
||||
import { value1 } from "module1";
|
||||
import { value2 } from "module2";
|
||||
console.log(value1, value2);
|
||||
`,
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: "pluginA",
|
||||
setup(builder) {
|
||||
// Resolve module1 to namespace plugin-a
|
||||
builder.onResolve({ filter: /^module1$/ }, args => {
|
||||
callOrder.push("pluginA-resolve");
|
||||
return {
|
||||
path: "shared-path.js",
|
||||
namespace: "plugin-a",
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pluginB",
|
||||
setup(builder) {
|
||||
// Resolve module2 to namespace plugin-b
|
||||
builder.onResolve({ filter: /^module2$/ }, args => {
|
||||
callOrder.push("pluginB-resolve");
|
||||
return {
|
||||
path: "shared-path.js",
|
||||
namespace: "plugin-b",
|
||||
};
|
||||
});
|
||||
|
||||
// Handle onLoad for namespace plugin-a
|
||||
builder.onLoad({ filter: /.*/, namespace: "plugin-a" }, args => {
|
||||
callOrder.push("pluginB-load-a");
|
||||
return {
|
||||
contents: 'export const value1 = "from plugin-a namespace";',
|
||||
loader: "js",
|
||||
};
|
||||
});
|
||||
|
||||
// Handle onLoad for namespace plugin-b
|
||||
builder.onLoad({ filter: /.*/, namespace: "plugin-b" }, args => {
|
||||
callOrder.push("pluginB-load-b");
|
||||
return {
|
||||
contents: 'export const value2 = "from plugin-b namespace";',
|
||||
loader: "js",
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
run: {
|
||||
stdout: "from plugin-a namespace from plugin-b namespace",
|
||||
},
|
||||
onAfterBundle() {
|
||||
// Both onLoad callbacks should have been called with their respective namespaces
|
||||
expect(callOrder).toEqual(["pluginA-resolve", "pluginB-resolve", "pluginB-load-a", "pluginB-load-b"]);
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,65 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { tls as options } from "harness";
|
||||
import { bunEnv, bunExe, tls as options } from "harness";
|
||||
import https from "https";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import tls from "tls";
|
||||
import { WebSocketServer } from "ws";
|
||||
test("should not crash when closing sockets after upgrade", async () => {
|
||||
|
||||
test.concurrent("WebSocket upgrade should unref poll_ref from response", async () => {
|
||||
// Regression test for bug where poll_ref was not unref'd on WebSocket upgrade
|
||||
// The bug: NodeHTTPResponse.poll_ref stayed active after upgrade
|
||||
// This test verifies activeTasks is correctly decremented after upgrade
|
||||
const script = /* js */ `
|
||||
const http = require("http");
|
||||
const { WebSocketServer } = require("ws");
|
||||
const { getEventLoopStats } = require("bun:internal-for-testing");
|
||||
|
||||
const server = http.createServer();
|
||||
const wsServer = new WebSocketServer({ server });
|
||||
|
||||
let initialStats;
|
||||
process.exitCode = 1;
|
||||
|
||||
wsServer.on("connection", (ws) => {
|
||||
// After WebSocket upgrade completes, check active tasks
|
||||
const stats = getEventLoopStats();
|
||||
ws.close();
|
||||
wsServer.close();
|
||||
server.close();
|
||||
|
||||
// With the bug: poll_ref from NodeHTTPResponse stays active (activeTasks = 1)
|
||||
// With the fix: poll_ref.unref() was called on upgrade (activeTasks should be 0)
|
||||
if (stats.activeTasks !== initialStats.activeTasks) {
|
||||
console.error("BUG_DETECTED: activeTasks=" + stats.activeTasks + " (expected 0 after upgrade)");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.exitCode = 0;
|
||||
});
|
||||
|
||||
initialStats = getEventLoopStats();
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const port = server.address().port;
|
||||
const ws = new WebSocket("ws://127.0.0.1:" + port);
|
||||
});
|
||||
`;
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "-e", script],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
|
||||
|
||||
// Should exit cleanly without detecting the bug
|
||||
expect(stderr).not.toContain("BUG_DETECTED");
|
||||
expect(stderr).toBe("");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test.concurrent("should not crash when closing sockets after upgrade", async () => {
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
let http_sockets: tls.TLSSocket[] = [];
|
||||
|
||||
|
||||
@@ -554,6 +554,386 @@ static napi_value test_is_typedarray(const Napi::CallbackInfo &info) {
|
||||
return ok(env);
|
||||
}
|
||||
|
||||
static napi_value test_napi_get_default_values(const Napi::CallbackInfo &info) {
|
||||
napi_env env = info.Env();
|
||||
|
||||
#ifndef _WIN32
|
||||
BlockingStdoutScope stdout_scope;
|
||||
#endif
|
||||
|
||||
napi_value obj;
|
||||
NODE_API_CALL(env, napi_create_object(env, &obj));
|
||||
|
||||
// Test 1: Get property that doesn't exist (should return undefined)
|
||||
napi_value unknown_key;
|
||||
NODE_API_CALL(env, napi_create_string_utf8(env, "nonexistent",
|
||||
NAPI_AUTO_LENGTH, &unknown_key));
|
||||
|
||||
napi_value result;
|
||||
napi_status get_status = napi_get_property(env, obj, unknown_key, &result);
|
||||
|
||||
if (get_status == napi_ok) {
|
||||
napi_valuetype result_type;
|
||||
napi_status type_status = napi_typeof(env, result, &result_type);
|
||||
|
||||
if (type_status == napi_ok && result_type == napi_undefined) {
|
||||
printf("PASS: napi_get_property for unknown key returned undefined\n");
|
||||
} else {
|
||||
printf("FAIL: napi_get_property for unknown key returned type %d "
|
||||
"(expected napi_undefined)\n",
|
||||
result_type);
|
||||
}
|
||||
} else {
|
||||
printf("FAIL: napi_get_property for unknown key failed with status %d\n",
|
||||
get_status);
|
||||
}
|
||||
|
||||
// Test 2: Get element at index that doesn't exist on array
|
||||
napi_value array;
|
||||
NODE_API_CALL(env, napi_create_array_with_length(env, 2, &array));
|
||||
|
||||
napi_value element_result;
|
||||
napi_status element_status = napi_get_element(env, array, 5, &element_result);
|
||||
|
||||
if (element_status == napi_ok) {
|
||||
napi_valuetype element_type;
|
||||
napi_status element_type_status =
|
||||
napi_typeof(env, element_result, &element_type);
|
||||
|
||||
if (element_type_status == napi_ok && element_type == napi_undefined) {
|
||||
printf("PASS: napi_get_element for out-of-bounds index returned "
|
||||
"undefined\n");
|
||||
} else {
|
||||
printf("FAIL: napi_get_element for out-of-bounds index returned type %d "
|
||||
"(expected napi_undefined)\n",
|
||||
element_type);
|
||||
}
|
||||
} else {
|
||||
printf("FAIL: napi_get_element for out-of-bounds index failed with status "
|
||||
"%d\n",
|
||||
element_status);
|
||||
}
|
||||
|
||||
// Test 3: Get named property that doesn't exist
|
||||
napi_value named_result;
|
||||
napi_status named_status =
|
||||
napi_get_named_property(env, obj, "missing_prop", &named_result);
|
||||
|
||||
if (named_status == napi_ok) {
|
||||
napi_valuetype named_type;
|
||||
napi_status named_type_status = napi_typeof(env, named_result, &named_type);
|
||||
|
||||
if (named_type_status == napi_ok && named_type == napi_undefined) {
|
||||
printf("PASS: napi_get_named_property for unknown property returned "
|
||||
"undefined\n");
|
||||
} else {
|
||||
printf("FAIL: napi_get_named_property for unknown property returned type "
|
||||
"%d (expected napi_undefined)\n",
|
||||
named_type);
|
||||
}
|
||||
} else {
|
||||
printf("FAIL: napi_get_named_property for unknown property failed with "
|
||||
"status %d\n",
|
||||
named_status);
|
||||
}
|
||||
|
||||
// Test 4: Set a property and verify we can get it back
|
||||
napi_value test_key;
|
||||
napi_value test_value;
|
||||
NODE_API_CALL(env, napi_create_string_utf8(env, "test_key", NAPI_AUTO_LENGTH,
|
||||
&test_key));
|
||||
NODE_API_CALL(env, napi_create_int32(env, 42, &test_value));
|
||||
|
||||
NODE_API_CALL(env, napi_set_property(env, obj, test_key, test_value));
|
||||
|
||||
napi_value retrieved_value;
|
||||
NODE_API_CALL(env, napi_get_property(env, obj, test_key, &retrieved_value));
|
||||
|
||||
int32_t retrieved_int;
|
||||
napi_status int_status =
|
||||
napi_get_value_int32(env, retrieved_value, &retrieved_int);
|
||||
|
||||
if (int_status == napi_ok && retrieved_int == 42) {
|
||||
printf("PASS: napi_get_property correctly retrieved set value: %d\n",
|
||||
retrieved_int);
|
||||
} else {
|
||||
printf("FAIL: napi_get_property did not retrieve correct value (got %d, "
|
||||
"expected 42)\n",
|
||||
retrieved_int);
|
||||
}
|
||||
|
||||
// Test 5: Use integer as property key (should be converted to string)
|
||||
napi_value int_key;
|
||||
napi_value int_key_value;
|
||||
NODE_API_CALL(env, napi_create_int32(env, 123, &int_key));
|
||||
NODE_API_CALL(env, napi_create_string_utf8(env, "integer_key_value",
|
||||
NAPI_AUTO_LENGTH, &int_key_value));
|
||||
|
||||
// Set property using integer key
|
||||
napi_status int_key_set_status =
|
||||
napi_set_property(env, obj, int_key, int_key_value);
|
||||
|
||||
if (int_key_set_status == napi_ok) {
|
||||
printf("PASS: napi_set_property with integer key succeeded\n");
|
||||
|
||||
// Try to get it back using the same integer key
|
||||
napi_value int_key_result;
|
||||
napi_status int_key_get_status =
|
||||
napi_get_property(env, obj, int_key, &int_key_result);
|
||||
|
||||
if (int_key_get_status == napi_ok) {
|
||||
// Check if we got back a string
|
||||
napi_valuetype int_key_result_type;
|
||||
napi_status int_key_type_status =
|
||||
napi_typeof(env, int_key_result, &int_key_result_type);
|
||||
|
||||
if (int_key_type_status == napi_ok &&
|
||||
int_key_result_type == napi_string) {
|
||||
char buffer[256];
|
||||
size_t copied;
|
||||
napi_status str_status = napi_get_value_string_utf8(
|
||||
env, int_key_result, buffer, sizeof(buffer), &copied);
|
||||
|
||||
if (str_status == napi_ok && strcmp(buffer, "integer_key_value") == 0) {
|
||||
printf("PASS: napi_get_property with integer key retrieved correct "
|
||||
"value: %s\n",
|
||||
buffer);
|
||||
} else {
|
||||
printf("FAIL: napi_get_property with integer key retrieved wrong "
|
||||
"value: %s\n",
|
||||
buffer);
|
||||
}
|
||||
} else {
|
||||
printf("FAIL: napi_get_property with integer key returned type %d "
|
||||
"(expected string)\n",
|
||||
int_key_result_type);
|
||||
}
|
||||
} else {
|
||||
printf("FAIL: napi_get_property with integer key failed with status %d\n",
|
||||
int_key_get_status);
|
||||
}
|
||||
|
||||
// Also try to get it using string "123"
|
||||
napi_value string_123_key;
|
||||
NODE_API_CALL(env, napi_create_string_utf8(env, "123", NAPI_AUTO_LENGTH,
|
||||
&string_123_key));
|
||||
|
||||
napi_value string_key_result;
|
||||
napi_status string_key_get_status =
|
||||
napi_get_property(env, obj, string_123_key, &string_key_result);
|
||||
|
||||
if (string_key_get_status == napi_ok) {
|
||||
napi_valuetype string_key_result_type;
|
||||
napi_status string_key_type_status =
|
||||
napi_typeof(env, string_key_result, &string_key_result_type);
|
||||
|
||||
if (string_key_type_status == napi_ok &&
|
||||
string_key_result_type == napi_string) {
|
||||
char buffer2[256];
|
||||
size_t copied2;
|
||||
napi_status str_status2 = napi_get_value_string_utf8(
|
||||
env, string_key_result, buffer2, sizeof(buffer2), &copied2);
|
||||
|
||||
if (str_status2 == napi_ok &&
|
||||
strcmp(buffer2, "integer_key_value") == 0) {
|
||||
printf("PASS: napi_get_property with string '123' key also retrieved "
|
||||
"correct value: %s\n",
|
||||
buffer2);
|
||||
} else {
|
||||
printf("FAIL: napi_get_property with string '123' key retrieved "
|
||||
"wrong value: %s\n",
|
||||
buffer2);
|
||||
}
|
||||
} else {
|
||||
printf("FAIL: napi_get_property with string '123' key returned type %d "
|
||||
"(expected string)\n",
|
||||
string_key_result_type);
|
||||
}
|
||||
} else {
|
||||
printf("FAIL: napi_get_property with string '123' key failed with status "
|
||||
"%d\n",
|
||||
string_key_get_status);
|
||||
}
|
||||
} else {
|
||||
printf("FAIL: napi_set_property with integer key failed with status %d\n",
|
||||
int_key_set_status);
|
||||
}
|
||||
|
||||
return ok(env);
|
||||
}
|
||||
|
||||
static napi_value
|
||||
test_napi_numeric_string_keys(const Napi::CallbackInfo &info) {
|
||||
napi_env env = info.Env();
|
||||
|
||||
#ifndef _WIN32
|
||||
BlockingStdoutScope stdout_scope;
|
||||
#endif
|
||||
|
||||
napi_value obj;
|
||||
NODE_API_CALL(env, napi_create_object(env, &obj));
|
||||
|
||||
// Test setting property with numeric string key "0"
|
||||
napi_value value_123;
|
||||
NODE_API_CALL(env, napi_create_int32(env, 123, &value_123));
|
||||
|
||||
napi_status set_status = napi_set_named_property(env, obj, "0", value_123);
|
||||
if (set_status == napi_ok) {
|
||||
printf("PASS: napi_set_named_property with key '0' succeeded\n");
|
||||
} else {
|
||||
printf("FAIL: napi_set_named_property with key '0' failed: %d\n",
|
||||
set_status);
|
||||
}
|
||||
|
||||
// Test has property with numeric string key "0"
|
||||
bool has_prop;
|
||||
napi_status has_status = napi_has_named_property(env, obj, "0", &has_prop);
|
||||
if (has_status == napi_ok && has_prop) {
|
||||
printf("PASS: napi_has_named_property with key '0' returned true\n");
|
||||
} else {
|
||||
printf("FAIL: napi_has_named_property with key '0' failed or returned "
|
||||
"false: status=%d, has=%s\n",
|
||||
has_status, has_prop ? "true" : "false");
|
||||
}
|
||||
|
||||
// Test getting property with numeric string key "0"
|
||||
napi_value retrieved_value;
|
||||
napi_status get_status =
|
||||
napi_get_named_property(env, obj, "0", &retrieved_value);
|
||||
if (get_status == napi_ok) {
|
||||
int32_t retrieved_int;
|
||||
napi_status int_status =
|
||||
napi_get_value_int32(env, retrieved_value, &retrieved_int);
|
||||
if (int_status == napi_ok && retrieved_int == 123) {
|
||||
printf("PASS: napi_get_named_property with key '0' returned correct "
|
||||
"value: %d\n",
|
||||
retrieved_int);
|
||||
} else {
|
||||
printf("FAIL: napi_get_named_property with key '0' returned wrong value: "
|
||||
"status=%d, value=%d\n",
|
||||
int_status, retrieved_int);
|
||||
}
|
||||
} else {
|
||||
printf("FAIL: napi_get_named_property with key '0' failed: %d\n",
|
||||
get_status);
|
||||
}
|
||||
|
||||
// Test with another numeric string key "1"
|
||||
napi_value value_456;
|
||||
NODE_API_CALL(env, napi_create_int32(env, 456, &value_456));
|
||||
|
||||
set_status = napi_set_named_property(env, obj, "1", value_456);
|
||||
if (set_status == napi_ok) {
|
||||
printf("PASS: napi_set_named_property with key '1' succeeded\n");
|
||||
} else {
|
||||
printf("FAIL: napi_set_named_property with key '1' failed: %d\n",
|
||||
set_status);
|
||||
}
|
||||
|
||||
has_status = napi_has_named_property(env, obj, "1", &has_prop);
|
||||
if (has_status == napi_ok && has_prop) {
|
||||
printf("PASS: napi_has_named_property with key '1' returned true\n");
|
||||
} else {
|
||||
printf("FAIL: napi_has_named_property with key '1' failed or returned "
|
||||
"false: status=%d, has=%s\n",
|
||||
has_status, has_prop ? "true" : "false");
|
||||
}
|
||||
|
||||
get_status = napi_get_named_property(env, obj, "1", &retrieved_value);
|
||||
if (get_status == napi_ok) {
|
||||
int32_t retrieved_int;
|
||||
napi_status int_status =
|
||||
napi_get_value_int32(env, retrieved_value, &retrieved_int);
|
||||
if (int_status == napi_ok && retrieved_int == 456) {
|
||||
printf("PASS: napi_get_named_property with key '1' returned correct "
|
||||
"value: %d\n",
|
||||
retrieved_int);
|
||||
} else {
|
||||
printf("FAIL: napi_get_named_property with key '1' returned wrong value: "
|
||||
"status=%d, value=%d\n",
|
||||
int_status, retrieved_int);
|
||||
}
|
||||
} else {
|
||||
printf("FAIL: napi_get_named_property with key '1' failed: %d\n",
|
||||
get_status);
|
||||
}
|
||||
|
||||
// Test with napi_get_property using numeric string keys
|
||||
napi_value key_0, key_1;
|
||||
NODE_API_CALL(env,
|
||||
napi_create_string_utf8(env, "0", NAPI_AUTO_LENGTH, &key_0));
|
||||
NODE_API_CALL(env,
|
||||
napi_create_string_utf8(env, "1", NAPI_AUTO_LENGTH, &key_1));
|
||||
|
||||
napi_value prop_value;
|
||||
napi_status prop_status = napi_get_property(env, obj, key_0, &prop_value);
|
||||
if (prop_status == napi_ok) {
|
||||
int32_t prop_int;
|
||||
napi_status int_status = napi_get_value_int32(env, prop_value, &prop_int);
|
||||
if (int_status == napi_ok && prop_int == 123) {
|
||||
printf(
|
||||
"PASS: napi_get_property with key '0' returned correct value: %d\n",
|
||||
prop_int);
|
||||
} else {
|
||||
printf("FAIL: napi_get_property with key '0' returned wrong value: "
|
||||
"status=%d, value=%d\n",
|
||||
int_status, prop_int);
|
||||
}
|
||||
} else {
|
||||
printf("FAIL: napi_get_property with key '0' failed: %d\n", prop_status);
|
||||
}
|
||||
|
||||
// Test napi_has_property
|
||||
bool has_property;
|
||||
napi_status has_prop_status =
|
||||
napi_has_property(env, obj, key_1, &has_property);
|
||||
if (has_prop_status == napi_ok && has_property) {
|
||||
printf("PASS: napi_has_property with key '1' returned true\n");
|
||||
} else {
|
||||
printf("FAIL: napi_has_property with key '1' failed or returned false: "
|
||||
"status=%d, has=%s\n",
|
||||
has_prop_status, has_property ? "true" : "false");
|
||||
}
|
||||
|
||||
// Test napi_has_own_property
|
||||
bool has_own_property;
|
||||
napi_status has_own_status =
|
||||
napi_has_own_property(env, obj, key_0, &has_own_property);
|
||||
if (has_own_status == napi_ok && has_own_property) {
|
||||
printf("PASS: napi_has_own_property with key '0' returned true\n");
|
||||
} else {
|
||||
printf("FAIL: napi_has_own_property with key '0' failed or returned false: "
|
||||
"status=%d, has=%s\n",
|
||||
has_own_status, has_own_property ? "true" : "false");
|
||||
}
|
||||
|
||||
// Test napi_delete_property
|
||||
bool delete_result;
|
||||
napi_status delete_status =
|
||||
napi_delete_property(env, obj, key_1, &delete_result);
|
||||
if (delete_status == napi_ok) {
|
||||
printf("PASS: napi_delete_property with key '1' succeeded, result=%s\n",
|
||||
delete_result ? "true" : "false");
|
||||
|
||||
// Verify the property was actually deleted
|
||||
bool still_has_property;
|
||||
napi_status verify_status =
|
||||
napi_has_property(env, obj, key_1, &still_has_property);
|
||||
if (verify_status == napi_ok && !still_has_property) {
|
||||
printf("PASS: Property '1' was successfully deleted\n");
|
||||
} else {
|
||||
printf(
|
||||
"FAIL: Property '1' still exists after deletion: status=%d, has=%s\n",
|
||||
verify_status, still_has_property ? "true" : "false");
|
||||
}
|
||||
} else {
|
||||
printf("FAIL: napi_delete_property with key '1' failed: %d\n",
|
||||
delete_status);
|
||||
}
|
||||
|
||||
return ok(env);
|
||||
}
|
||||
|
||||
static napi_value test_deferred_exceptions(const Napi::CallbackInfo &info) {
|
||||
napi_env env = info.Env();
|
||||
|
||||
@@ -1466,6 +1846,8 @@ void register_standalone_tests(Napi::Env env, Napi::Object exports) {
|
||||
REGISTER_FUNCTION(env, exports, bigint_to_64_null);
|
||||
REGISTER_FUNCTION(env, exports, test_is_buffer);
|
||||
REGISTER_FUNCTION(env, exports, test_is_typedarray);
|
||||
REGISTER_FUNCTION(env, exports, test_napi_get_default_values);
|
||||
REGISTER_FUNCTION(env, exports, test_napi_numeric_string_keys);
|
||||
REGISTER_FUNCTION(env, exports, test_deferred_exceptions);
|
||||
REGISTER_FUNCTION(env, exports, test_napi_strict_equals);
|
||||
REGISTER_FUNCTION(env, exports, test_napi_call_function_recv_null);
|
||||
|
||||
@@ -549,6 +549,14 @@ describe.concurrent("napi", () => {
|
||||
await checkSameOutput("test_deferred_exceptions", []);
|
||||
});
|
||||
|
||||
it("behaves as expected when performing operations with numeric string keys", async () => {
|
||||
await checkSameOutput("test_napi_numeric_string_keys", []);
|
||||
});
|
||||
|
||||
it("behaves as expected when performing operations with default values", async () => {
|
||||
await checkSameOutput("test_napi_get_default_values", []);
|
||||
});
|
||||
|
||||
it("NAPI finalizer iterator invalidation crash prevention", () => {
|
||||
// This test verifies that the DeferGCForAWhile fix prevents iterator invalidation
|
||||
// during NAPI finalizer cleanup. While we couldn't reproduce the exact crash
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { test } from "bun:test";
|
||||
import { basename, dirname, sep } from "node:path";
|
||||
import { build, run } from "../../../harness";
|
||||
|
||||
@@ -6,7 +7,7 @@ test("build", async () => {
|
||||
});
|
||||
|
||||
for (const file of Array.from(new Bun.Glob("*.js").scanSync(import.meta.dir))) {
|
||||
test.todoIf(["test.js"].includes(file))(file, () => {
|
||||
test(file, () => {
|
||||
run(dirname(import.meta.dir), basename(import.meta.dir) + sep + file);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user