Compare commits

..

5 Commits

Author SHA1 Message Date
Dylan Conway
cf6cdbbbad Revert "Mimalloc v3 update (#26379)" (#26783)
This reverts commit c63415c9c9.

### What does this PR do?

### How did you verify your code works?
2026-02-06 18:05:17 -08:00
robobun
89d2b1cd0b fix(websocket): add missing incPendingActivityCount() in blob binaryType case (#26670)
## Summary

- Fix crash ("Pure virtual function called!") when WebSocket client
receives binary data with `binaryType = "blob"` and no event listener
attached
- Add missing `incPendingActivityCount()` call before `postTask` in the
Blob case of `didReceiveBinaryData`
- Add regression test for issue #26669

## Root Cause

The Blob case in `didReceiveBinaryData` (WebSocket.cpp:1324-1331) was
calling `decPendingActivityCount()` inside the `postTask` callback
without a matching `incPendingActivityCount()` beforehand. This bug was
introduced in #21471 when Blob support was added.

The ArrayBuffer and NodeBuffer cases correctly call
`incPendingActivityCount()` before `postTask`, but the Blob case was
missing this call.

## Test plan

- [x] New regression test verifies WebSocket with `binaryType = "blob"`
doesn't crash on ping frames
- [x] `bun bd test test/regression/issue/26669.test.ts` passes

Fixes #26669

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: Ciro Spaciari MacBook <ciro@anthropic.com>
2026-02-05 20:39:19 -08:00
Jarred Sumner
2019a1b11d Bump WebKit 2026-02-05 20:09:39 -08:00
SUZUKI Sosuke
6c70ce2485 Update WebKit to 7bc2f97e28353062bb54776ce01e4c2ff24c35cc (#26769)
### What does this PR do?

### How did you verify your code works?
2026-02-05 17:58:30 -08:00
SUZUKI Sosuke
0e386c4168 fix(stringWidth): correct width for Thai/Lao spacing vowels (#26728)
## Summary

`Bun.stringWidth` was incorrectly treating Thai SARA AA (U+0E32), SARA
AM (U+0E33), and their Lao equivalents (U+0EB2, U+0EB3) as zero-width
characters.

## Root Cause

In `src/string/immutable/visible.zig`, the range check for Thai/Lao
combining marks was too broad:
- Thai: `0xe31 <= cp <= 0xe3a` included U+0E32 and U+0E33
- Lao: `0xeb1 <= cp <= 0xebc` included U+0EB2 and U+0EB3

According to Unicode (UCD Grapheme_Break property), these are **spacing
vowels** (Grapheme_Base), not combining marks.

## Changes

- **`src/string/immutable/visible.zig`**: Exclude U+0E32, U+0E33,
U+0EB2, U+0EB3 from zero-width ranges
- **`test/js/bun/util/stringWidth.test.ts`**: Add tests for Thai and Lao
spacing vowels

## Before/After

| Character | Before | After |
|-----------|--------|-------|
| `\u0E32` (SARA AA) | 0 | 1 |
| `\u0E33` (SARA AM) | 0 | 1 |
| `คำ` (common Thai word) | 1 | 2 |
| `\u0EB2` (Lao AA) | 0 | 1 |
| `\u0EB3` (Lao AM) | 0 | 1 |

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-05 17:31:15 -08:00
14 changed files with 202 additions and 1172 deletions

View File

@@ -4,7 +4,7 @@ register_repository(
REPOSITORY
oven-sh/mimalloc
COMMIT
ffa38ab8ac914f9eb7af75c1f8ad457643dc14f2
1beadf9651a7bfdec6b5367c380ecc3fe1c40d1a
)
set(MIMALLOC_CMAKE_ARGS
@@ -14,7 +14,7 @@ set(MIMALLOC_CMAKE_ARGS
-DMI_BUILD_TESTS=OFF
-DMI_USE_CXX=ON
-DMI_SKIP_COLLECT_ON_EXIT=ON
# ```
# mimalloc_allow_large_os_pages=0 BUN_PORT=3004 mem bun http-hello.js
# Started development server: http://localhost:3004
@@ -51,7 +51,7 @@ if(ENABLE_ASAN)
list(APPEND MIMALLOC_CMAKE_ARGS -DMI_DEBUG_UBSAN=ON)
elseif(APPLE OR LINUX)
if(APPLE)
list(APPEND MIMALLOC_CMAKE_ARGS -DMI_OVERRIDE=OFF)
list(APPEND MIMALLOC_CMAKE_ARGS -DMI_OVERRIDE=OFF)
list(APPEND MIMALLOC_CMAKE_ARGS -DMI_OSX_ZONE=OFF)
list(APPEND MIMALLOC_CMAKE_ARGS -DMI_OSX_INTERPOSE=OFF)
else()
@@ -87,9 +87,9 @@ endif()
if(WIN32)
if(DEBUG)
set(MIMALLOC_LIBRARY mimalloc-debug)
set(MIMALLOC_LIBRARY mimalloc-static-debug)
else()
set(MIMALLOC_LIBRARY mimalloc)
set(MIMALLOC_LIBRARY mimalloc-static)
endif()
elseif(DEBUG)
if (ENABLE_ASAN)

View File

@@ -6,7 +6,7 @@ option(WEBKIT_LOCAL "If a local version of WebKit should be used instead of down
option(WEBKIT_BUILD_TYPE "The build type for local WebKit (defaults to CMAKE_BUILD_TYPE)")
if(NOT WEBKIT_VERSION)
set(WEBKIT_VERSION 7bc2f97e28353062bb54776ce01e4c2ff24c35cc)
set(WEBKIT_VERSION 8af7958ff0e2a4787569edf64641a1ae7cfe074a)
endif()
# Use preview build URL for Windows ARM64 until the fix is merged to main

View File

@@ -610,129 +610,6 @@ declare module "bun" {
*/
function stripANSI(input: string): string;
/**
* Converts a string to camelCase.
*
* @param input The string to convert.
* @returns The camelCase version of the string.
* @example
* ```ts
* Bun.camelCase("foo bar") // "fooBar"
* Bun.camelCase("XMLParser") // "xmlParser"
* ```
*/
function camelCase(input: string): string;
/**
* Converts a string to PascalCase.
*
* @param input The string to convert.
* @returns The PascalCase version of the string.
* @example
* ```ts
* Bun.pascalCase("foo bar") // "FooBar"
* ```
*/
function pascalCase(input: string): string;
/**
* Converts a string to snake_case.
*
* @param input The string to convert.
* @returns The snake_case version of the string.
* @example
* ```ts
* Bun.snakeCase("fooBar") // "foo_bar"
* ```
*/
function snakeCase(input: string): string;
/**
* Converts a string to kebab-case.
*
* @param input The string to convert.
* @returns The kebab-case version of the string.
* @example
* ```ts
* Bun.kebabCase("fooBar") // "foo-bar"
* ```
*/
function kebabCase(input: string): string;
/**
* Converts a string to CONSTANT_CASE.
*
* @param input The string to convert.
* @returns The CONSTANT_CASE version of the string.
* @example
* ```ts
* Bun.constantCase("fooBar") // "FOO_BAR"
* ```
*/
function constantCase(input: string): string;
/**
* Converts a string to dot.case.
*
* @param input The string to convert.
* @returns The dot.case version of the string.
* @example
* ```ts
* Bun.dotCase("fooBar") // "foo.bar"
* ```
*/
function dotCase(input: string): string;
/**
* Converts a string to Capital Case.
*
* @param input The string to convert.
* @returns The Capital Case version of the string.
* @example
* ```ts
* Bun.capitalCase("fooBar") // "Foo Bar"
* ```
*/
function capitalCase(input: string): string;
/**
* Converts a string to Train-Case.
*
* @param input The string to convert.
* @returns The Train-Case version of the string.
* @example
* ```ts
* Bun.trainCase("fooBar") // "Foo-Bar"
* ```
*/
function trainCase(input: string): string;
/**
* Converts a string to path/case.
*
* @param input The string to convert.
* @returns The path/case version of the string.
* @example
* ```ts
* Bun.pathCase("fooBar") // "foo/bar"
* ```
*/
function pathCase(input: string): string;
/**
* Converts a string to Sentence case.
*
* @param input The string to convert.
* @returns The Sentence case version of the string.
* @example
* ```ts
* Bun.sentenceCase("fooBar") // "Foo bar"
* ```
*/
function sentenceCase(input: string): string;
/**
* Converts a string to no case (lowercased words separated by spaces).
*
* @param input The string to convert.
* @returns The no case version of the string.
* @example
* ```ts
* Bun.noCase("fooBar") // "foo bar"
* ```
*/
function noCase(input: string): string;
interface WrapAnsiOptions {
/**
* If `true`, break words in the middle if they don't fit on a line.

View File

@@ -2,10 +2,7 @@
const Self = @This();
const safety_checks = bun.Environment.isDebug or bun.Environment.enable_asan;
#heap: *mimalloc.Heap,
thread_id: if (safety_checks) std.Thread.Id else void,
#heap: if (safety_checks) Owned(*DebugHeap) else *mimalloc.Heap,
/// Uses the default thread-local heap. This type is zero-sized.
///
@@ -23,18 +20,18 @@ pub const Default = struct {
///
/// This type is a `GenericAllocator`; see `src/allocators.zig`.
pub const Borrowed = struct {
#heap: *mimalloc.Heap,
#heap: BorrowedHeap,
pub fn allocator(self: Borrowed) std.mem.Allocator {
return .{ .ptr = self.#heap, .vtable = c_allocator_vtable };
return .{ .ptr = self.#heap, .vtable = &c_allocator_vtable };
}
pub fn getDefault() Borrowed {
return .{ .#heap = mimalloc.mi_heap_main() };
return .{ .#heap = getThreadHeap() };
}
pub fn gc(self: Borrowed) void {
mimalloc.mi_heap_collect(self.#heap, false);
mimalloc.mi_heap_collect(self.getMimallocHeap(), false);
}
pub fn helpCatchMemoryIssues(self: Borrowed) void {
@@ -44,17 +41,30 @@ pub const Borrowed = struct {
}
}
pub fn ownsPtr(self: Borrowed, ptr: *const anyopaque) bool {
return mimalloc.mi_heap_check_owned(self.getMimallocHeap(), ptr);
}
fn fromOpaque(ptr: *anyopaque) Borrowed {
return .{ .#heap = @ptrCast(@alignCast(ptr)) };
}
fn getMimallocHeap(self: Borrowed) *mimalloc.Heap {
return if (comptime safety_checks) self.#heap.inner else self.#heap;
}
fn assertThreadLock(self: Borrowed) void {
if (comptime safety_checks) self.#heap.thread_lock.assertLocked();
}
fn alignedAlloc(self: Borrowed, len: usize, alignment: Alignment) ?[*]u8 {
log("Malloc: {d}\n", .{len});
const heap = self.getMimallocHeap();
const ptr: ?*anyopaque = if (mimalloc.mustUseAlignedAlloc(alignment))
mimalloc.mi_heap_malloc_aligned(self.#heap, len, alignment.toByteUnits())
mimalloc.mi_heap_malloc_aligned(heap, len, alignment.toByteUnits())
else
mimalloc.mi_heap_malloc(self.#heap, len);
mimalloc.mi_heap_malloc(heap, len);
if (comptime bun.Environment.isDebug) {
const usable = mimalloc.mi_malloc_usable_size(ptr);
@@ -79,17 +89,42 @@ pub const Borrowed = struct {
}
};
const BorrowedHeap = if (safety_checks) *DebugHeap else *mimalloc.Heap;
const DebugHeap = struct {
inner: *mimalloc.Heap,
thread_lock: bun.safety.ThreadLock,
pub const deinit = void;
};
threadlocal var thread_heap: if (safety_checks) ?DebugHeap else void = if (safety_checks) null;
fn getThreadHeap() BorrowedHeap {
if (comptime !safety_checks) return mimalloc.mi_heap_get_default();
if (thread_heap == null) {
thread_heap = .{
.inner = mimalloc.mi_heap_get_default(),
.thread_lock = .initLocked(),
};
}
return &thread_heap.?;
}
const log = bun.Output.scoped(.mimalloc, .hidden);
pub fn allocator(self: Self) std.mem.Allocator {
self.assertThreadOwnership();
return self.borrow().allocator();
}
pub fn borrow(self: Self) Borrowed {
return .{ .#heap = self.#heap };
return .{ .#heap = if (comptime safety_checks) self.#heap.get() else self.#heap };
}
/// Internally, mimalloc calls mi_heap_get_default()
/// to get the default heap.
/// It uses pthread_getspecific to do that.
/// We can save those extra calls if we just do it once in here
pub fn getThreadLocalDefault() std.mem.Allocator {
if (bun.Environment.enable_asan) return bun.default_allocator;
return Borrowed.getDefault().allocator();
@@ -122,15 +157,22 @@ pub fn dumpStats(_: Self) void {
}
pub fn deinit(self: *Self) void {
mimalloc.mi_heap_destroy(self.#heap);
const mimalloc_heap = self.borrow().getMimallocHeap();
if (comptime safety_checks) {
self.#heap.deinit();
}
mimalloc.mi_heap_destroy(mimalloc_heap);
self.* = undefined;
}
pub fn init() Self {
return .{
.#heap = mimalloc.mi_heap_new() orelse bun.outOfMemory(),
.thread_id = if (safety_checks) std.Thread.getCurrentId() else {},
};
const mimalloc_heap = mimalloc.mi_heap_new() orelse bun.outOfMemory();
if (comptime !safety_checks) return .{ .#heap = mimalloc_heap };
const heap: Owned(*DebugHeap) = .new(.{
.inner = mimalloc_heap,
.thread_lock = .initLocked(),
});
return .{ .#heap = heap };
}
pub fn gc(self: Self) void {
@@ -141,16 +183,8 @@ pub fn helpCatchMemoryIssues(self: Self) void {
self.borrow().helpCatchMemoryIssues();
}
fn assertThreadOwnership(self: Self) void {
if (comptime safety_checks) {
const current_thread = std.Thread.getCurrentId();
if (current_thread != self.thread_id) {
std.debug.panic(
"MimallocArena used from wrong thread: arena belongs to thread {d}, but current thread is {d}",
.{ self.thread_id, current_thread },
);
}
}
pub fn ownsPtr(self: Self, ptr: *const anyopaque) bool {
return self.borrow().ownsPtr(ptr);
}
fn alignedAllocSize(ptr: [*]u8) usize {
@@ -159,10 +193,13 @@ fn alignedAllocSize(ptr: [*]u8) usize {
fn vtable_alloc(ptr: *anyopaque, len: usize, alignment: Alignment, _: usize) ?[*]u8 {
const self: Borrowed = .fromOpaque(ptr);
self.assertThreadLock();
return self.alignedAlloc(len, alignment);
}
fn vtable_resize(_: *anyopaque, buf: []u8, _: Alignment, new_len: usize, _: usize) bool {
fn vtable_resize(ptr: *anyopaque, buf: []u8, _: Alignment, new_len: usize, _: usize) bool {
const self: Borrowed = .fromOpaque(ptr);
self.assertThreadLock();
return mimalloc.mi_expand(buf.ptr, new_len) != null;
}
@@ -186,17 +223,39 @@ fn vtable_free(
}
}
/// Attempt to expand or shrink memory, allowing relocation.
///
/// `memory.len` must equal the length requested from the most recent
/// successful call to `alloc`, `resize`, or `remap`. `alignment` must
/// equal the same value that was passed as the `alignment` parameter to
/// the original `alloc` call.
///
/// A non-`null` return value indicates the resize was successful. The
/// allocation may have same address, or may have been relocated. In either
/// case, the allocation now has size of `new_len`. A `null` return value
/// indicates that the resize would be equivalent to allocating new memory,
/// copying the bytes from the old memory, and then freeing the old memory.
/// In such case, it is more efficient for the caller to perform the copy.
///
/// `new_len` must be greater than zero.
///
/// `ret_addr` is optionally provided as the first return address of the
/// allocation call stack. If the value is `0` it means no return address
/// has been provided.
fn vtable_remap(ptr: *anyopaque, buf: []u8, alignment: Alignment, new_len: usize, _: usize) ?[*]u8 {
const self: Borrowed = .fromOpaque(ptr);
const value = mimalloc.mi_heap_realloc_aligned(self.#heap, buf.ptr, new_len, alignment.toByteUnits());
self.assertThreadLock();
const heap = self.getMimallocHeap();
const aligned_size = alignment.toByteUnits();
const value = mimalloc.mi_heap_realloc_aligned(heap, buf.ptr, new_len, aligned_size);
return @ptrCast(value);
}
pub fn isInstance(alloc: std.mem.Allocator) bool {
return alloc.vtable == c_allocator_vtable;
return alloc.vtable == &c_allocator_vtable;
}
const c_allocator_vtable = &std.mem.Allocator.VTable{
const c_allocator_vtable = std.mem.Allocator.VTable{
.alloc = vtable_alloc,
.resize = vtable_resize,
.remap = vtable_remap,
@@ -209,3 +268,5 @@ const Alignment = std.mem.Alignment;
const bun = @import("bun");
const assert = bun.assert;
const mimalloc = bun.mimalloc;
const Owned = bun.ptr.Owned;
const safety_checks = bun.Environment.ci_assert;

View File

@@ -60,29 +60,17 @@ pub const Heap = opaque {
return mi_heap_realloc(self, p, newsize);
}
pub fn isOwned(self: *Heap, p: ?*const anyopaque) bool {
return mi_heap_contains(self, p);
pub fn isOwned(self: *Heap, p: ?*anyopaque) bool {
return mi_heap_check_owned(self, p);
}
};
pub extern fn mi_heap_new() ?*Heap;
pub extern fn mi_heap_delete(heap: *Heap) void;
pub extern fn mi_heap_destroy(heap: *Heap) void;
pub extern fn mi_heap_set_default(heap: *Heap) *Heap;
pub extern fn mi_heap_get_default() *Heap;
pub extern fn mi_heap_get_backing() *Heap;
pub extern fn mi_heap_collect(heap: *Heap, force: bool) void;
pub extern fn mi_heap_main() *Heap;
// Thread-local heap (theap) API - new in mimalloc v3
pub const THeap = opaque {};
pub extern fn mi_theap_get_default() *THeap;
pub extern fn mi_theap_set_default(theap: *THeap) *THeap;
pub extern fn mi_theap_collect(theap: *THeap, force: bool) void;
pub extern fn mi_theap_malloc(theap: *THeap, size: usize) ?*anyopaque;
pub extern fn mi_theap_zalloc(theap: *THeap, size: usize) ?*anyopaque;
pub extern fn mi_theap_calloc(theap: *THeap, count: usize, size: usize) ?*anyopaque;
pub extern fn mi_theap_malloc_small(theap: *THeap, size: usize) ?*anyopaque;
pub extern fn mi_theap_malloc_aligned(theap: *THeap, size: usize, alignment: usize) ?*anyopaque;
pub extern fn mi_theap_realloc(theap: *THeap, p: ?*anyopaque, newsize: usize) ?*anyopaque;
pub extern fn mi_theap_destroy(theap: *THeap) void;
pub extern fn mi_heap_theap(heap: *Heap) *THeap;
pub extern fn mi_heap_malloc(heap: *Heap, size: usize) ?*anyopaque;
pub extern fn mi_heap_zalloc(heap: *Heap, size: usize) ?*anyopaque;
pub extern fn mi_heap_calloc(heap: *Heap, count: usize, size: usize) ?*anyopaque;
@@ -114,7 +102,8 @@ pub extern fn mi_heap_rezalloc_aligned(heap: *Heap, p: ?*anyopaque, newsize: usi
pub extern fn mi_heap_rezalloc_aligned_at(heap: *Heap, p: ?*anyopaque, newsize: usize, alignment: usize, offset: usize) ?*anyopaque;
pub extern fn mi_heap_recalloc_aligned(heap: *Heap, p: ?*anyopaque, newcount: usize, size: usize, alignment: usize) ?*anyopaque;
pub extern fn mi_heap_recalloc_aligned_at(heap: *Heap, p: ?*anyopaque, newcount: usize, size: usize, alignment: usize, offset: usize) ?*anyopaque;
pub extern fn mi_heap_contains(heap: *const Heap, p: ?*const anyopaque) bool;
pub extern fn mi_heap_contains_block(heap: *Heap, p: *const anyopaque) bool;
pub extern fn mi_heap_check_owned(heap: *Heap, p: *const anyopaque) bool;
pub extern fn mi_check_owned(p: ?*const anyopaque) bool;
pub const struct_mi_heap_area_s = extern struct {
blocks: ?*anyopaque,

View File

@@ -82,8 +82,6 @@ JSC_DECLARE_HOST_FUNCTION(jsFunctionBunStripANSI);
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunWrapAnsi);
}
#include "CaseChange.h"
using namespace JSC;
using namespace WebCore;
@@ -934,18 +932,14 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
allocUnsafe BunObject_callback_allocUnsafe DontDelete|Function 1
argv BunObject_lazyPropCb_wrap_argv DontDelete|PropertyCallback
build BunObject_callback_build DontDelete|Function 1
camelCase jsFunctionBunCamelCase DontDelete|Function 1
capitalCase jsFunctionBunCapitalCase DontDelete|Function 1
concatArrayBuffers functionConcatTypedArrays DontDelete|Function 3
connect BunObject_callback_connect DontDelete|Function 1
constantCase jsFunctionBunConstantCase DontDelete|Function 1
cwd BunObject_lazyPropCb_wrap_cwd DontEnum|DontDelete|PropertyCallback
color BunObject_callback_color DontDelete|Function 2
deepEquals functionBunDeepEquals DontDelete|Function 2
deepMatch functionBunDeepMatch DontDelete|Function 2
deflateSync BunObject_callback_deflateSync DontDelete|Function 1
dns constructDNSObject ReadOnly|DontDelete|PropertyCallback
dotCase jsFunctionBunDotCase DontDelete|Function 1
enableANSIColors BunObject_lazyPropCb_wrap_enableANSIColors DontDelete|PropertyCallback
env constructEnvObject ReadOnly|DontDelete|PropertyCallback
escapeHTML functionBunEscapeHTML DontDelete|Function 2
@@ -960,7 +954,6 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
indexOfLine BunObject_callback_indexOfLine DontDelete|Function 1
inflateSync BunObject_callback_inflateSync DontDelete|Function 1
inspect BunObject_lazyPropCb_wrap_inspect DontDelete|PropertyCallback
kebabCase jsFunctionBunKebabCase DontDelete|Function 1
isMainThread constructIsMainThread ReadOnly|DontDelete|PropertyCallback
jest BunObject_callback_jest DontEnum|DontDelete|Function 1
listen BunObject_callback_listen DontDelete|Function 1
@@ -968,10 +961,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
main bunObjectMain DontDelete|CustomAccessor
mmap BunObject_callback_mmap DontDelete|Function 1
nanoseconds functionBunNanoseconds DontDelete|Function 0
noCase jsFunctionBunNoCase DontDelete|Function 1
openInEditor BunObject_callback_openInEditor DontDelete|Function 1
pascalCase jsFunctionBunPascalCase DontDelete|Function 1
pathCase jsFunctionBunPathCase DontDelete|Function 1
origin BunObject_lazyPropCb_wrap_origin DontEnum|ReadOnly|DontDelete|PropertyCallback
version_with_sha constructBunVersionWithSha DontEnum|ReadOnly|DontDelete|PropertyCallback
password constructPasswordObject DontDelete|PropertyCallback
@@ -992,8 +982,6 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
resolveSync BunObject_callback_resolveSync DontDelete|Function 1
revision constructBunRevision ReadOnly|DontDelete|PropertyCallback
semver BunObject_lazyPropCb_wrap_semver ReadOnly|DontDelete|PropertyCallback
sentenceCase jsFunctionBunSentenceCase DontDelete|Function 1
snakeCase jsFunctionBunSnakeCase DontDelete|Function 1
sql defaultBunSQLObject DontDelete|PropertyCallback
postgres defaultBunSQLObject DontDelete|PropertyCallback
SQL constructBunSQLObject DontDelete|PropertyCallback
@@ -1009,7 +997,6 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
stdout BunObject_lazyPropCb_wrap_stdout DontDelete|PropertyCallback
stringWidth Generated::BunObject::jsStringWidth DontDelete|Function 2
stripANSI jsFunctionBunStripANSI DontDelete|Function 1
trainCase jsFunctionBunTrainCase DontDelete|Function 1
wrapAnsi jsFunctionBunWrapAnsi DontDelete|Function 3
Terminal BunObject_lazyPropCb_wrap_Terminal DontDelete|PropertyCallback
unsafe BunObject_lazyPropCb_wrap_unsafe DontDelete|PropertyCallback

View File

@@ -1,355 +0,0 @@
#include "root.h"
#include "CaseChange.h"
#include <unicode/uchar.h>
#include <unicode/utf16.h>
#include <wtf/text/StringBuilder.h>
#include <wtf/text/WTFString.h>
namespace Bun {
using namespace JSC;
using namespace WTF;
enum class CaseType {
Camel,
Pascal,
Snake,
Kebab,
Constant,
Dot,
Capital,
Train,
Path,
Sentence,
No
};
enum class CharClass {
Lower,
Upper,
Digit,
Other
};
enum class WordTransform {
Lower,
Upper,
Capitalize
};
static inline CharClass classifyCp(char32_t c)
{
if (c < 0x80) {
if (c >= 'a' && c <= 'z')
return CharClass::Lower;
if (c >= 'A' && c <= 'Z')
return CharClass::Upper;
if (c >= '0' && c <= '9')
return CharClass::Digit;
return CharClass::Other;
}
if (u_hasBinaryProperty(c, UCHAR_UPPERCASE))
return CharClass::Upper;
if (u_hasBinaryProperty(c, UCHAR_ALPHABETIC))
return CharClass::Lower;
return CharClass::Other;
}
static inline char separator(CaseType type)
{
switch (type) {
case CaseType::Camel:
case CaseType::Pascal:
return 0;
case CaseType::Snake:
case CaseType::Constant:
return '_';
case CaseType::Kebab:
case CaseType::Train:
return '-';
case CaseType::Dot:
return '.';
case CaseType::Capital:
case CaseType::Sentence:
case CaseType::No:
return ' ';
case CaseType::Path:
return '/';
}
RELEASE_ASSERT_NOT_REACHED();
}
static inline bool hasDigitPrefixUnderscore(CaseType type)
{
return type == CaseType::Camel || type == CaseType::Pascal;
}
static inline WordTransform getTransform(CaseType type, size_t wordIndex)
{
switch (type) {
case CaseType::Camel:
return wordIndex == 0 ? WordTransform::Lower : WordTransform::Capitalize;
case CaseType::Pascal:
return WordTransform::Capitalize;
case CaseType::Snake:
case CaseType::Kebab:
case CaseType::Dot:
case CaseType::Path:
case CaseType::No:
return WordTransform::Lower;
case CaseType::Constant:
return WordTransform::Upper;
case CaseType::Capital:
case CaseType::Train:
return WordTransform::Capitalize;
case CaseType::Sentence:
return wordIndex == 0 ? WordTransform::Capitalize : WordTransform::Lower;
}
RELEASE_ASSERT_NOT_REACHED();
}
// Word boundary detection and case conversion, templated on character type.
// For Latin1Character, each element is a codepoint.
// For UChar, we use U16_NEXT to handle surrogate pairs.
template<typename CharType>
static WTF::String convertCase(CaseType type, std::span<const CharType> input)
{
// First pass: collect word boundaries (start/end byte offsets)
struct WordRange {
uint32_t start;
uint32_t end;
};
Vector<WordRange, 16> words;
{
bool inWord = false;
uint32_t wordStart = 0;
uint32_t wordEnd = 0;
CharClass prevClass = CharClass::Other;
CharClass prevPrevClass = CharClass::Other;
uint32_t prevPos = 0;
int32_t i = 0;
int32_t length = static_cast<int32_t>(input.size());
while (i < length) {
uint32_t curPos = static_cast<uint32_t>(i);
char32_t cp;
if constexpr (std::is_same_v<CharType, Latin1Character>) {
cp = input[i];
i++;
} else {
U16_NEXT(input.data(), i, length, cp);
}
uint32_t curEnd = static_cast<uint32_t>(i);
CharClass curClass = classifyCp(cp);
if (curClass == CharClass::Other) {
if (inWord) {
inWord = false;
words.append({ wordStart, wordEnd });
prevClass = CharClass::Other;
prevPrevClass = CharClass::Other;
} else {
prevClass = CharClass::Other;
prevPrevClass = CharClass::Other;
}
continue;
}
if (!inWord) {
inWord = true;
wordStart = curPos;
wordEnd = curEnd;
prevPrevClass = CharClass::Other;
prevClass = curClass;
prevPos = curPos;
continue;
}
// Rule 2: upper+upper+lower → boundary before the last upper
if (prevPrevClass == CharClass::Upper && prevClass == CharClass::Upper && curClass == CharClass::Lower) {
words.append({ wordStart, prevPos });
wordStart = prevPos;
wordEnd = curEnd;
prevPrevClass = prevClass;
prevClass = curClass;
prevPos = curPos;
continue;
}
// Rule 1: (lower | digit) → upper boundary
if ((prevClass == CharClass::Lower || prevClass == CharClass::Digit) && curClass == CharClass::Upper) {
words.append({ wordStart, wordEnd });
wordStart = curPos;
wordEnd = curEnd;
prevPrevClass = CharClass::Other;
prevClass = curClass;
prevPos = curPos;
continue;
}
// No boundary, extend current word
wordEnd = curEnd;
prevPrevClass = prevClass;
prevClass = curClass;
prevPos = curPos;
}
// Flush last word
if (inWord)
words.append({ wordStart, wordEnd });
}
if (words.isEmpty())
return emptyString();
// Second pass: build the output string
StringBuilder builder;
builder.reserveCapacity(input.size() + input.size() / 4);
char sep = separator(type);
for (size_t wordIndex = 0; wordIndex < words.size(); wordIndex++) {
auto& word = words[wordIndex];
// Separator between words
if (wordIndex > 0 && sep)
builder.append(sep);
// Digit-prefix underscore for camelCase/pascalCase
if (wordIndex > 0 && hasDigitPrefixUnderscore(type)) {
char32_t firstCp;
if constexpr (std::is_same_v<CharType, Latin1Character>) {
firstCp = input[word.start];
} else {
int32_t tmpI = word.start;
U16_NEXT(input.data(), tmpI, static_cast<int32_t>(input.size()), firstCp);
}
if (firstCp >= '0' && firstCp <= '9')
builder.append('_');
}
WordTransform transform = getTransform(type, wordIndex);
// Iterate codepoints within the word and apply transform
int32_t pos = word.start;
int32_t end = word.end;
bool isFirst = true;
while (pos < end) {
char32_t cp;
if constexpr (std::is_same_v<CharType, Latin1Character>) {
cp = input[pos];
pos++;
} else {
U16_NEXT(input.data(), pos, end, cp);
}
char32_t transformed;
switch (transform) {
case WordTransform::Lower:
transformed = u_tolower(cp);
break;
case WordTransform::Upper:
transformed = u_toupper(cp);
break;
case WordTransform::Capitalize:
transformed = isFirst ? u_toupper(cp) : u_tolower(cp);
break;
}
isFirst = false;
builder.append(static_cast<char32_t>(transformed));
}
}
return builder.toString();
}
static EncodedJSValue caseChangeImpl(CaseType type, JSGlobalObject* globalObject, CallFrame* callFrame)
{
auto& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
JSValue input = callFrame->argument(0);
if (!input.isString()) {
throwTypeError(globalObject, scope, "Expected a string argument"_s);
return {};
}
JSString* jsStr = input.toString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
auto view = jsStr->view(globalObject);
RETURN_IF_EXCEPTION(scope, {});
if (view->isEmpty())
return JSValue::encode(jsEmptyString(vm));
WTF::String result = view->is8Bit()
? convertCase<Latin1Character>(type, view->span8())
: convertCase<UChar>(type, view->span16());
return JSValue::encode(jsString(vm, WTF::move(result)));
}
JSC_DEFINE_HOST_FUNCTION(jsFunctionBunCamelCase, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
return caseChangeImpl(CaseType::Camel, globalObject, callFrame);
}
JSC_DEFINE_HOST_FUNCTION(jsFunctionBunPascalCase, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
return caseChangeImpl(CaseType::Pascal, globalObject, callFrame);
}
JSC_DEFINE_HOST_FUNCTION(jsFunctionBunSnakeCase, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
return caseChangeImpl(CaseType::Snake, globalObject, callFrame);
}
JSC_DEFINE_HOST_FUNCTION(jsFunctionBunKebabCase, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
return caseChangeImpl(CaseType::Kebab, globalObject, callFrame);
}
JSC_DEFINE_HOST_FUNCTION(jsFunctionBunConstantCase, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
return caseChangeImpl(CaseType::Constant, globalObject, callFrame);
}
JSC_DEFINE_HOST_FUNCTION(jsFunctionBunDotCase, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
return caseChangeImpl(CaseType::Dot, globalObject, callFrame);
}
JSC_DEFINE_HOST_FUNCTION(jsFunctionBunCapitalCase, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
return caseChangeImpl(CaseType::Capital, globalObject, callFrame);
}
JSC_DEFINE_HOST_FUNCTION(jsFunctionBunTrainCase, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
return caseChangeImpl(CaseType::Train, globalObject, callFrame);
}
JSC_DEFINE_HOST_FUNCTION(jsFunctionBunPathCase, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
return caseChangeImpl(CaseType::Path, globalObject, callFrame);
}
JSC_DEFINE_HOST_FUNCTION(jsFunctionBunSentenceCase, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
return caseChangeImpl(CaseType::Sentence, globalObject, callFrame);
}
JSC_DEFINE_HOST_FUNCTION(jsFunctionBunNoCase, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
return caseChangeImpl(CaseType::No, globalObject, callFrame);
}
} // namespace Bun

View File

@@ -1,19 +0,0 @@
#pragma once
#include "root.h"
namespace Bun {
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunCamelCase);
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunPascalCase);
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunSnakeCase);
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunKebabCase);
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunConstantCase);
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunDotCase);
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunCapitalCase);
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunTrainCase);
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunPathCase);
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunSentenceCase);
JSC_DECLARE_HOST_FUNCTION(jsFunctionBunNoCase);
}

View File

@@ -1323,6 +1323,7 @@ void WebSocket::didReceiveBinaryData(const AtomString& eventName, const std::spa
if (auto* context = scriptExecutionContext()) {
RefPtr<Blob> blob = Blob::create(binaryData, context->jsGlobalObject());
this->incPendingActivityCount();
context->postTask([this, name = eventName, blob = blob.releaseNonNull(), protectedThis = Ref { *this }](ScriptExecutionContext& context) {
ASSERT(scriptExecutionContext());
protectedThis->dispatchEvent(MessageEvent::create(name, blob, protectedThis->m_url.string()));

View File

@@ -70,11 +70,13 @@ pub fn isZeroWidthCodepointType(comptime T: type, cp: T) bool {
}
// Thai combining marks
if ((cp >= 0xe31 and cp <= 0xe3a) or (cp >= 0xe47 and cp <= 0xe4e))
// Note: U+0E32 (SARA AA) and U+0E33 (SARA AM) are Grapheme_Base (spacing vowels), not combining
if (cp == 0xe31 or (cp >= 0xe34 and cp <= 0xe3a) or (cp >= 0xe47 and cp <= 0xe4e))
return true;
// Lao combining marks
if ((cp >= 0xeb1 and cp <= 0xebc) or (cp >= 0xec8 and cp <= 0xecd))
// Note: U+0EB2 and U+0EB3 are spacing vowels like Thai, not combining
if (cp == 0xeb1 or (cp >= 0xeb4 and cp <= 0xebc) or (cp >= 0xec8 and cp <= 0xecd))
return true;
// Combining Diacritical Marks Extended

View File

@@ -68,6 +68,6 @@ describe("static initializers", () => {
expect(
bunInitializers.length,
`Do not add static initializers to Bun. Static initializers are called when Bun starts up, regardless of whether you use the variables or not. This makes Bun slower.`,
).toBe(process.arch === "arm64" ? 2 : 3);
).toBe(process.arch === "arm64" ? 1 : 2);
});
});

View File

@@ -1,604 +0,0 @@
import { describe, expect, test } from "bun:test";
import {
camelCase,
capitalCase,
constantCase,
dotCase,
kebabCase,
noCase,
pascalCase,
pathCase,
sentenceCase,
snakeCase,
trainCase,
} from "change-case";
type CaseFn = (input: string) => string;
const bunFns: Record<string, CaseFn> = {
camelCase: Bun.camelCase,
capitalCase: Bun.capitalCase,
constantCase: Bun.constantCase,
dotCase: Bun.dotCase,
kebabCase: Bun.kebabCase,
noCase: Bun.noCase,
pascalCase: Bun.pascalCase,
pathCase: Bun.pathCase,
sentenceCase: Bun.sentenceCase,
snakeCase: Bun.snakeCase,
trainCase: Bun.trainCase,
};
const changeCaseFns: Record<string, CaseFn> = {
camelCase,
capitalCase,
constantCase,
dotCase,
kebabCase,
noCase,
pascalCase,
pathCase,
sentenceCase,
snakeCase,
trainCase,
};
// Comprehensive input set covering many patterns
const testInputs = [
// Basic words
"test",
"foo",
"a",
"",
// Multi-word with various separators
"test string",
"test_string",
"test-string",
"test.string",
"test/string",
"test\tstring",
// Cased inputs
"Test String",
"TEST STRING",
"TestString",
"testString",
"TEST_STRING",
// Acronyms and consecutive uppercase
"XMLParser",
"getHTTPSURL",
"parseJSON",
"simpleXML",
"PDFLoader",
"I18N",
"ABC",
"ABCdef",
"ABCDef",
"HTMLElement",
"innerHTML",
"XMLHttpRequest",
"getURLParams",
"isHTTPS",
"CSSStyleSheet",
"IOError",
"UIKit",
// Numbers
"version 1.2.10",
"TestV2",
"test123",
"123test",
"test 123 value",
"1st place",
"v2beta1",
"ES6Module",
"utf8Decode",
"base64Encode",
"h1Element",
"int32Array",
"123",
"123 456",
"a1b2c3",
"test0",
// Multiple separators / weird spacing
"foo___bar",
"foo---bar",
"foo...bar",
"foo bar",
" leading spaces ",
"__private",
"--dashed--",
"..dotted..",
" ",
"\t\ttabs\t\t",
"foo_-_bar",
"foo.-bar",
// All uppercase
"FOO_BAR_BAZ",
"ALLCAPS",
"FOO BAR",
"FOO-BAR",
"FOO.BAR",
// Mixed case
"fooBarBaz",
"FooBarBaz",
"Foo Bar",
"MiXeD CaSe",
"already camelCase",
"already PascalCase",
"already_snake_case",
"already-kebab-case",
"Already Capital Case",
"ALREADY_CONSTANT_CASE",
// Pre-formatted cases
"Train-Case-Input",
"dot.case.input",
"path/case/input",
"Sentence case input",
"no case input",
// Single characters
"A",
"z",
"Z",
"0",
// Real-world identifiers
"backgroundColor",
"border-top-color",
"MAX_RETRY_COUNT",
"Content-Type",
"X-Forwarded-For",
"user_id",
"getUserById",
"class_name",
"className",
"is_active",
"isActive",
"created_at",
"createdAt",
"HTTPSConnection",
"myXMLParser",
"getDBConnection",
"setHTTPSEnabled",
"enableSSL",
"useGPU",
"readCSV",
"parseHTML",
"toJSON",
"fromURL",
"isNaN",
"toString",
"valueOf",
// Column name style inputs (SQL)
"first_name",
"last_name",
"email_address",
"phone_number",
"order_total",
"created_at",
"updated_at",
"is_deleted",
// Hyphenated compound words
"well-known",
"read-only",
"built-in",
"self-contained",
// Strings with only separators
"---",
"___",
"...",
"///",
"-_.-_.",
// Unicode (basic)
"café latte",
"naïve résumé",
"hello 世界",
// Long strings
"this is a much longer test string with many words to convert",
"thisIsAMuchLongerTestStringWithManyWordsToConvert",
"THIS_IS_A_MUCH_LONGER_TEST_STRING_WITH_MANY_WORDS_TO_CONVERT",
];
const allCaseNames = Object.keys(bunFns);
describe("case-change", () => {
// Main compatibility matrix: every function x every input
for (const caseName of allCaseNames) {
const bunFn = bunFns[caseName];
const changeCaseFn = changeCaseFns[caseName];
describe(caseName, () => {
for (const input of testInputs) {
const expected = changeCaseFn(input);
test(`${JSON.stringify(input)} => ${JSON.stringify(expected)}`, () => {
expect(bunFn(input)).toBe(expected);
});
}
});
}
// Cross-conversion round-trips: convert from A to B, compare both implementations
describe("cross-conversion round-trips", () => {
const conversions = [
"camelCase",
"pascalCase",
"snakeCase",
"kebabCase",
"constantCase",
"noCase",
"dotCase",
] as const;
const roundTripInputs = [
"hello world",
"fooBarBaz",
"FOO_BAR",
"XMLParser",
"getHTTPSURL",
"test_string",
"Test String",
"already-kebab",
"version 1.2.10",
];
for (const input of roundTripInputs) {
for (const from of conversions) {
for (const to of conversions) {
const intermediate = changeCaseFns[from](input);
const expected = changeCaseFns[to](intermediate);
test(`${from}(${JSON.stringify(input)}) => ${to}`, () => {
const bunIntermediate = bunFns[from](input);
expect(bunFns[to](bunIntermediate)).toBe(expected);
});
}
}
}
});
// Double-conversion stability: converting the output again should be idempotent
describe("idempotency", () => {
const idempotentInputs = ["hello world", "fooBarBaz", "FOO_BAR_BAZ", "XMLParser", "test 123", "café latte"];
for (const caseName of allCaseNames) {
const bunFn = bunFns[caseName];
const changeCaseFn = changeCaseFns[caseName];
for (const input of idempotentInputs) {
test(`${caseName}(${caseName}(${JSON.stringify(input)})) is idempotent`, () => {
const once = bunFn(input);
const twice = bunFn(once);
const expectedOnce = changeCaseFn(input);
const expectedTwice = changeCaseFn(expectedOnce);
expect(once).toBe(expectedOnce);
expect(twice).toBe(expectedTwice);
});
}
}
});
// Specific per-function expected values (hardcoded, not generated)
describe("specific expected values", () => {
test("camelCase", () => {
expect(Bun.camelCase("foo bar")).toBe("fooBar");
expect(Bun.camelCase("foo-bar")).toBe("fooBar");
expect(Bun.camelCase("foo_bar")).toBe("fooBar");
expect(Bun.camelCase("FOO_BAR")).toBe("fooBar");
expect(Bun.camelCase("FooBar")).toBe("fooBar");
expect(Bun.camelCase("fooBar")).toBe("fooBar");
expect(Bun.camelCase("")).toBe("");
expect(Bun.camelCase("foo")).toBe("foo");
expect(Bun.camelCase("A")).toBe("a");
});
test("pascalCase", () => {
expect(Bun.pascalCase("foo bar")).toBe("FooBar");
expect(Bun.pascalCase("foo-bar")).toBe("FooBar");
expect(Bun.pascalCase("foo_bar")).toBe("FooBar");
expect(Bun.pascalCase("FOO_BAR")).toBe("FooBar");
expect(Bun.pascalCase("fooBar")).toBe("FooBar");
expect(Bun.pascalCase("")).toBe("");
expect(Bun.pascalCase("foo")).toBe("Foo");
});
test("snakeCase", () => {
expect(Bun.snakeCase("foo bar")).toBe("foo_bar");
expect(Bun.snakeCase("fooBar")).toBe("foo_bar");
expect(Bun.snakeCase("FooBar")).toBe("foo_bar");
expect(Bun.snakeCase("FOO_BAR")).toBe("foo_bar");
expect(Bun.snakeCase("foo-bar")).toBe("foo_bar");
expect(Bun.snakeCase("")).toBe("");
});
test("kebabCase", () => {
expect(Bun.kebabCase("foo bar")).toBe("foo-bar");
expect(Bun.kebabCase("fooBar")).toBe("foo-bar");
expect(Bun.kebabCase("FooBar")).toBe("foo-bar");
expect(Bun.kebabCase("FOO_BAR")).toBe("foo-bar");
expect(Bun.kebabCase("foo_bar")).toBe("foo-bar");
expect(Bun.kebabCase("")).toBe("");
});
test("constantCase", () => {
expect(Bun.constantCase("foo bar")).toBe("FOO_BAR");
expect(Bun.constantCase("fooBar")).toBe("FOO_BAR");
expect(Bun.constantCase("FooBar")).toBe("FOO_BAR");
expect(Bun.constantCase("foo-bar")).toBe("FOO_BAR");
expect(Bun.constantCase("foo_bar")).toBe("FOO_BAR");
expect(Bun.constantCase("")).toBe("");
});
test("dotCase", () => {
expect(Bun.dotCase("foo bar")).toBe("foo.bar");
expect(Bun.dotCase("fooBar")).toBe("foo.bar");
expect(Bun.dotCase("FOO_BAR")).toBe("foo.bar");
expect(Bun.dotCase("")).toBe("");
});
test("capitalCase", () => {
expect(Bun.capitalCase("foo bar")).toBe("Foo Bar");
expect(Bun.capitalCase("fooBar")).toBe("Foo Bar");
expect(Bun.capitalCase("FOO_BAR")).toBe("Foo Bar");
expect(Bun.capitalCase("")).toBe("");
});
test("trainCase", () => {
expect(Bun.trainCase("foo bar")).toBe("Foo-Bar");
expect(Bun.trainCase("fooBar")).toBe("Foo-Bar");
expect(Bun.trainCase("FOO_BAR")).toBe("Foo-Bar");
expect(Bun.trainCase("")).toBe("");
});
test("pathCase", () => {
expect(Bun.pathCase("foo bar")).toBe("foo/bar");
expect(Bun.pathCase("fooBar")).toBe("foo/bar");
expect(Bun.pathCase("FOO_BAR")).toBe("foo/bar");
expect(Bun.pathCase("")).toBe("");
});
test("sentenceCase", () => {
expect(Bun.sentenceCase("foo bar")).toBe("Foo bar");
expect(Bun.sentenceCase("fooBar")).toBe("Foo bar");
expect(Bun.sentenceCase("FOO_BAR")).toBe("Foo bar");
expect(Bun.sentenceCase("")).toBe("");
});
test("noCase", () => {
expect(Bun.noCase("foo bar")).toBe("foo bar");
expect(Bun.noCase("fooBar")).toBe("foo bar");
expect(Bun.noCase("FOO_BAR")).toBe("foo bar");
expect(Bun.noCase("FooBar")).toBe("foo bar");
expect(Bun.noCase("")).toBe("");
});
});
// Edge cases
describe("edge cases", () => {
test("empty string returns empty for all functions", () => {
for (const caseName of allCaseNames) {
expect(bunFns[caseName]("")).toBe("");
}
});
test("single character", () => {
for (const ch of ["a", "A", "z", "Z", "0", "9"]) {
for (const caseName of allCaseNames) {
expect(bunFns[caseName](ch)).toBe(changeCaseFns[caseName](ch));
}
}
});
test("all separators produce empty for all functions", () => {
for (const sep of ["---", "___", "...", " ", "\t\t", "-_.-_."]) {
for (const caseName of allCaseNames) {
expect(bunFns[caseName](sep)).toBe(changeCaseFns[caseName](sep));
}
}
});
test("numbers only", () => {
for (const input of ["123", "0", "999", "123 456", "1.2.3"]) {
for (const caseName of allCaseNames) {
expect(bunFns[caseName](input)).toBe(changeCaseFns[caseName](input));
}
}
});
test("mixed numbers and letters", () => {
for (const input of [
"test123",
"123test",
"test 123 value",
"1st place",
"v2beta1",
"a1b2c3",
"ES6Module",
"utf8Decode",
"base64Encode",
"h1Element",
"int32Array",
]) {
for (const caseName of allCaseNames) {
expect(bunFns[caseName](input)).toBe(changeCaseFns[caseName](input));
}
}
});
test("consecutive separators are collapsed", () => {
for (const input of ["foo___bar", "foo---bar", "foo...bar", "foo bar"]) {
expect(Bun.camelCase(input)).toBe(camelCase(input));
expect(Bun.snakeCase(input)).toBe(snakeCase(input));
}
});
test("leading and trailing separators are stripped", () => {
for (const input of [" foo ", "__bar__", "--baz--", "..qux.."]) {
for (const caseName of allCaseNames) {
expect(bunFns[caseName](input)).toBe(changeCaseFns[caseName](input));
}
}
});
test("unicode strings", () => {
for (const input of ["café latte", "naïve résumé", "hello 世界"]) {
for (const caseName of allCaseNames) {
expect(bunFns[caseName](input)).toBe(changeCaseFns[caseName](input));
}
}
});
test("acronym splitting", () => {
// These specifically test the upper->upper+lower boundary rule
for (const input of [
"XMLParser",
"HTMLElement",
"innerHTML",
"XMLHttpRequest",
"getURLParams",
"isHTTPS",
"CSSStyleSheet",
"IOError",
"UIKit",
"HTTPSConnection",
"myXMLParser",
"getDBConnection",
"setHTTPSEnabled",
"ABCDef",
"ABCdef",
]) {
for (const caseName of allCaseNames) {
expect(bunFns[caseName](input)).toBe(changeCaseFns[caseName](input));
}
}
});
test("digit-prefix underscore in camelCase/pascalCase", () => {
// change-case inserts _ before digit-starting words (index > 0) in camel/pascal
const input = "version 1.2.10";
expect(Bun.camelCase(input)).toBe(camelCase(input));
expect(Bun.pascalCase(input)).toBe(pascalCase(input));
// snake/kebab/etc should NOT have the _ prefix
expect(Bun.snakeCase(input)).toBe(snakeCase(input));
expect(Bun.kebabCase(input)).toBe(kebabCase(input));
});
test("long strings", () => {
const long =
"this is a much longer test string with many words to convert and it keeps going and going and going";
for (const caseName of allCaseNames) {
expect(bunFns[caseName](long)).toBe(changeCaseFns[caseName](long));
}
});
test("repeated single word", () => {
expect(Bun.camelCase("foo")).toBe("foo");
expect(Bun.pascalCase("foo")).toBe("Foo");
expect(Bun.snakeCase("foo")).toBe("foo");
expect(Bun.kebabCase("foo")).toBe("foo");
expect(Bun.constantCase("foo")).toBe("FOO");
});
test("single uppercase word", () => {
expect(Bun.camelCase("FOO")).toBe(camelCase("FOO"));
expect(Bun.pascalCase("FOO")).toBe(pascalCase("FOO"));
expect(Bun.snakeCase("FOO")).toBe(snakeCase("FOO"));
});
});
// Error handling
describe("error handling", () => {
for (const caseName of allCaseNames) {
const fn = bunFns[caseName];
test(`${caseName}() with no arguments throws`, () => {
// @ts-expect-error
expect(() => fn()).toThrow();
});
test(`${caseName}(123) with number throws`, () => {
// @ts-expect-error
expect(() => fn(123)).toThrow();
});
test(`${caseName}(null) throws`, () => {
// @ts-expect-error
expect(() => fn(null)).toThrow();
});
test(`${caseName}(undefined) throws`, () => {
// @ts-expect-error
expect(() => fn(undefined)).toThrow();
});
test(`${caseName}({}) with object throws`, () => {
// @ts-expect-error
expect(() => fn({})).toThrow();
});
test(`${caseName}([]) with array throws`, () => {
// @ts-expect-error
expect(() => fn([])).toThrow();
});
test(`${caseName}(true) with boolean throws`, () => {
// @ts-expect-error
expect(() => fn(true)).toThrow();
});
}
});
// Ensure .length property is 1
describe("function.length", () => {
for (const caseName of allCaseNames) {
test(`Bun.${caseName}.length === 1`, () => {
expect(bunFns[caseName].length).toBe(1);
});
}
});
// Stress test with generated inputs
describe("generated inputs", () => {
// Words joined with various separators
const words = ["foo", "bar", "baz", "qux"];
const separators = [" ", "_", "-", ".", "/", " ", "__", "--"];
for (const sep of separators) {
const input = words.join(sep);
test(`words joined by ${JSON.stringify(sep)}: ${JSON.stringify(input)}`, () => {
for (const caseName of allCaseNames) {
expect(bunFns[caseName](input)).toBe(changeCaseFns[caseName](input));
}
});
}
// Various camelCase-style inputs
const camelInputs = [
"oneTwoThree",
"OneTwoThree",
"oneTWOThree",
"ONETwoThree",
"oneTwo3",
"one2Three",
"one23",
"oneABCTwo",
];
for (const input of camelInputs) {
test(`camelCase-style: ${JSON.stringify(input)}`, () => {
for (const caseName of allCaseNames) {
expect(bunFns[caseName](input)).toBe(changeCaseFns[caseName](input));
}
});
}
});
});

View File

@@ -485,6 +485,28 @@ describe("stringWidth extended", () => {
expect(Bun.stringWidth("ก็")).toBe(1); // With maitaikhu
expect(Bun.stringWidth("ปฏัก")).toBe(3); // ป + ฏ + ั (combining) + ก = 3 visible
});
test("Thai spacing vowels (SARA AA and SARA AM)", () => {
// U+0E32 (SARA AA) and U+0E33 (SARA AM) are spacing vowels, not combining marks
expect(Bun.stringWidth("\u0E32")).toBe(1); // SARA AA alone
expect(Bun.stringWidth("\u0E33")).toBe(1); // SARA AM alone
expect(Bun.stringWidth("ก\u0E32")).toBe(2); // ก + SARA AA
expect(Bun.stringWidth("ก\u0E33")).toBe(2); // กำ (KO KAI + SARA AM)
expect(Bun.stringWidth("คำ")).toBe(2); // Common Thai word
expect(Bun.stringWidth("ทำ")).toBe(2); // Common Thai word
// True combining marks should still be zero-width
expect(Bun.stringWidth("\u0E31")).toBe(0); // MAI HAN-AKAT (combining)
expect(Bun.stringWidth("ก\u0E31")).toBe(1); // กั
});
test("Lao spacing vowels", () => {
// U+0EB2 and U+0EB3 are spacing vowels in Lao, similar to Thai
expect(Bun.stringWidth("\u0EB2")).toBe(1); // LAO VOWEL SIGN AA
expect(Bun.stringWidth("\u0EB3")).toBe(1); // LAO VOWEL SIGN AM
expect(Bun.stringWidth("ກ\u0EB2")).toBe(2); // KO + AA
// True combining marks should still be zero-width
expect(Bun.stringWidth("\u0EB1")).toBe(0); // MAI KAN (combining)
});
});
describe("non-ASCII in escape sequences and Indic script handling", () => {

View File

@@ -0,0 +1,69 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
// https://github.com/oven-sh/bun/issues/26669
// WebSocket client crashes ("Pure virtual function called!") when binaryType = "blob"
// and no event listener is attached. The missing incPendingActivityCount() allows the
// WebSocket to be GC'd before the postTask callback runs.
test("WebSocket with binaryType blob should not crash when GC'd before postTask", async () => {
await using server = Bun.serve({
port: 0,
fetch(req, server) {
if (server.upgrade(req)) return undefined;
return new Response("Not a websocket");
},
websocket: {
open(ws) {
// Send binary data immediately - this triggers didReceiveBinaryData
// with the Blob path when client has binaryType = "blob"
ws.sendBinary(new Uint8Array(64));
ws.sendBinary(new Uint8Array(64));
ws.sendBinary(new Uint8Array(64));
},
message() {},
},
});
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const url = process.argv[1];
// Create many short-lived WebSocket objects with blob binaryType and no listeners.
// Without the fix, the missing incPendingActivityCount() lets the WebSocket get GC'd
// before the postTask callback fires, causing "Pure virtual function called!".
async function run() {
for (let i = 0; i < 100; i++) {
const ws = new WebSocket(url);
ws.binaryType = "blob";
// Intentionally: NO event listeners attached.
// This forces the postTask path in didReceiveBinaryData's Blob case.
}
// Force GC to collect the unreferenced WebSocket objects while postTask
// callbacks are still pending.
Bun.gc(true);
await Bun.sleep(50);
Bun.gc(true);
await Bun.sleep(50);
Bun.gc(true);
await Bun.sleep(100);
}
await run();
Bun.gc(true);
await Bun.sleep(200);
console.log("OK");
process.exit(0);
`,
`ws://localhost:${server.port}`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("OK");
expect(exitCode).toBe(0);
});