mirror of
https://github.com/oven-sh/bun
synced 2026-02-18 06:41:50 +00:00
Compare commits
7 Commits
claude/fix
...
claude/str
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e125b725d | ||
|
|
72e1c6577c | ||
|
|
3cb5c4a722 | ||
|
|
30ca7c952b | ||
|
|
c153188363 | ||
|
|
7afead629c | ||
|
|
9a72bbfae2 |
68
bench/snippets/structuredClone-typedarray.mjs
Normal file
68
bench/snippets/structuredClone-typedarray.mjs
Normal file
@@ -0,0 +1,68 @@
|
||||
import { bench, group, run, summary } from "../runner.mjs";
|
||||
|
||||
// === TypedArray structuredClone benchmarks ===
|
||||
|
||||
// Uint8Array at various sizes
|
||||
var uint8_64 = new Uint8Array(64);
|
||||
var uint8_1K = new Uint8Array(1024);
|
||||
var uint8_64K = new Uint8Array(64 * 1024);
|
||||
var uint8_1M = new Uint8Array(1024 * 1024);
|
||||
|
||||
// Fill with non-zero data to be realistic
|
||||
for (var i = 0; i < uint8_64.length; i++) uint8_64[i] = i & 0xff;
|
||||
for (var i = 0; i < uint8_1K.length; i++) uint8_1K[i] = i & 0xff;
|
||||
for (var i = 0; i < uint8_64K.length; i++) uint8_64K[i] = i & 0xff;
|
||||
for (var i = 0; i < uint8_1M.length; i++) uint8_1M[i] = i & 0xff;
|
||||
|
||||
// Other typed array types (1KB each)
|
||||
var int8_1K = new Int8Array(1024);
|
||||
var uint16_1K = new Uint16Array(512); // 1KB
|
||||
var int32_1K = new Int32Array(256); // 1KB
|
||||
var float32_1K = new Float32Array(256); // 1KB
|
||||
var float64_1K = new Float64Array(128); // 1KB
|
||||
var bigint64_1K = new BigInt64Array(128); // 1KB
|
||||
|
||||
for (var i = 0; i < int8_1K.length; i++) int8_1K[i] = (i % 256) - 128;
|
||||
for (var i = 0; i < uint16_1K.length; i++) uint16_1K[i] = i;
|
||||
for (var i = 0; i < int32_1K.length; i++) int32_1K[i] = i * 1000;
|
||||
for (var i = 0; i < float32_1K.length; i++) float32_1K[i] = i * 0.1;
|
||||
for (var i = 0; i < float64_1K.length; i++) float64_1K[i] = i * 0.1;
|
||||
for (var i = 0; i < bigint64_1K.length; i++) bigint64_1K[i] = BigInt(i);
|
||||
|
||||
// Slice view (byteOffset != 0) — should fall back to slow path
|
||||
var sliceBuf = new ArrayBuffer(2048);
|
||||
var uint8_slice = new Uint8Array(sliceBuf, 512, 512);
|
||||
|
||||
summary(() => {
|
||||
group("Uint8Array by size", () => {
|
||||
bench("Uint8Array 64B", () => structuredClone(uint8_64));
|
||||
bench("Uint8Array 1KB", () => structuredClone(uint8_1K));
|
||||
bench("Uint8Array 64KB", () => structuredClone(uint8_64K));
|
||||
bench("Uint8Array 1MB", () => structuredClone(uint8_1M));
|
||||
});
|
||||
});
|
||||
|
||||
summary(() => {
|
||||
group("TypedArray types (1KB each)", () => {
|
||||
bench("Int8Array", () => structuredClone(int8_1K));
|
||||
bench("Uint8Array", () => structuredClone(uint8_1K));
|
||||
bench("Uint16Array", () => structuredClone(uint16_1K));
|
||||
bench("Int32Array", () => structuredClone(int32_1K));
|
||||
bench("Float32Array", () => structuredClone(float32_1K));
|
||||
bench("Float64Array", () => structuredClone(float64_1K));
|
||||
bench("BigInt64Array", () => structuredClone(bigint64_1K));
|
||||
});
|
||||
});
|
||||
|
||||
// Pre-create for fair comparison
|
||||
var uint8_whole = new Uint8Array(512);
|
||||
for (var i = 0; i < 512; i++) uint8_whole[i] = i & 0xff;
|
||||
|
||||
summary(() => {
|
||||
group("fast path vs slow path (512B)", () => {
|
||||
bench("Uint8Array whole (fast path)", () => structuredClone(uint8_whole));
|
||||
bench("Uint8Array slice (slow path)", () => structuredClone(uint8_slice));
|
||||
});
|
||||
});
|
||||
|
||||
await run();
|
||||
@@ -59,4 +59,15 @@ var objectsMedium = Array.from({ length: 100 }, (_, i) => ({ id: i, name: `item-
|
||||
bench("structuredClone([10 objects])", () => structuredClone(objectsSmall));
|
||||
bench("structuredClone([100 objects])", () => structuredClone(objectsMedium));
|
||||
|
||||
// TypedArray fast path targets
|
||||
var uint8Small = new Uint8Array(64);
|
||||
var uint8Medium = new Uint8Array(1024);
|
||||
var uint8Large = new Uint8Array(1024 * 1024);
|
||||
var float64Medium = new Float64Array(128);
|
||||
|
||||
bench("structuredClone(Uint8Array 64B)", () => structuredClone(uint8Small));
|
||||
bench("structuredClone(Uint8Array 1KB)", () => structuredClone(uint8Medium));
|
||||
bench("structuredClone(Uint8Array 1MB)", () => structuredClone(uint8Large));
|
||||
bench("structuredClone(Float64Array 1KB)", () => structuredClone(float64Medium));
|
||||
|
||||
await run();
|
||||
|
||||
@@ -1265,6 +1265,7 @@ if(WIN32)
|
||||
target_link_libraries(${bun} PRIVATE
|
||||
${WEBKIT_LIB_PATH}/WTF.lib
|
||||
${WEBKIT_LIB_PATH}/JavaScriptCore.lib
|
||||
${WEBKIT_LIB_PATH}/bmalloc.lib
|
||||
${WEBKIT_LIB_PATH}/sicudtd.lib
|
||||
${WEBKIT_LIB_PATH}/sicuind.lib
|
||||
${WEBKIT_LIB_PATH}/sicuucd.lib
|
||||
@@ -1273,6 +1274,7 @@ if(WIN32)
|
||||
target_link_libraries(${bun} PRIVATE
|
||||
${WEBKIT_LIB_PATH}/WTF.lib
|
||||
${WEBKIT_LIB_PATH}/JavaScriptCore.lib
|
||||
${WEBKIT_LIB_PATH}/bmalloc.lib
|
||||
${WEBKIT_LIB_PATH}/sicudt.lib
|
||||
${WEBKIT_LIB_PATH}/sicuin.lib
|
||||
${WEBKIT_LIB_PATH}/sicuuc.lib
|
||||
|
||||
292
src/CLAUDE.md
292
src/CLAUDE.md
@@ -10,3 +10,295 @@ Conventions:
|
||||
- Prefer `@import` at the **bottom** of the file, but the auto formatter will move them so you don't need to worry about it.
|
||||
- **Never** use `@import()` inline inside of functions. **Always** put them at the bottom of the file or containing struct. Imports in Zig are free of side-effects, so there's no such thing as a "dynamic" import.
|
||||
- You must be patient with the build.
|
||||
|
||||
## Prefer Bun APIs over `std`
|
||||
|
||||
**Always use `bun.*` APIs instead of `std.*`.** The `bun` namespace (`@import("bun")`) provides cross-platform wrappers that preserve OS error info and never use `unreachable`. Using `std.fs`, `std.posix`, or `std.os` directly is wrong in this codebase.
|
||||
|
||||
| Instead of | Use |
|
||||
| ------------------------------------------------------------ | ------------------------------------ |
|
||||
| `std.fs.File` | `bun.sys.File` |
|
||||
| `std.fs.cwd()` | `bun.FD.cwd()` |
|
||||
| `std.posix.open/read/write/stat/mkdir/unlink/rename/symlink` | `bun.sys.*` equivalents |
|
||||
| `std.fs.path.join/dirname/basename` | `bun.path.join/dirname/basename` |
|
||||
| `std.mem.eql/indexOf/startsWith` (for strings) | `bun.strings.eql/indexOf/startsWith` |
|
||||
| `std.posix.O` / `std.posix.mode_t` / `std.posix.fd_t` | `bun.O` / `bun.Mode` / `bun.FD` |
|
||||
| `std.process.Child` | `bun.spawnSync` |
|
||||
| `catch bun.outOfMemory()` | `bun.handleOom(...)` |
|
||||
|
||||
## `bun.sys` — System Calls (`src/sys.zig`)
|
||||
|
||||
All return `Maybe(T)` — a tagged union of `.result: T` or `.err: bun.sys.Error`:
|
||||
|
||||
```zig
|
||||
const fd = switch (bun.sys.open(path, bun.O.RDONLY, 0)) {
|
||||
.result => |fd| fd,
|
||||
.err => |err| return .{ .err = err },
|
||||
};
|
||||
// Or: const fd = try bun.sys.open(path, bun.O.RDONLY, 0).unwrap();
|
||||
```
|
||||
|
||||
Key functions (all take `bun.FileDescriptor`, not `std.posix.fd_t`):
|
||||
|
||||
- `open`, `openat`, `openA` (non-sentinel) → `Maybe(bun.FileDescriptor)`
|
||||
- `read`, `readAll`, `pread` → `Maybe(usize)`
|
||||
- `write`, `pwrite`, `writev` → `Maybe(usize)`
|
||||
- `stat`, `fstat`, `lstat` → `Maybe(bun.Stat)`
|
||||
- `mkdir`, `unlink`, `rename`, `symlink`, `chmod`, `fchmod`, `fchown` → `Maybe(void)`
|
||||
- `readlink`, `getFdPath`, `getcwd` → `Maybe` of path slice
|
||||
- `getFileSize`, `dup`, `sendfile`, `mmap`
|
||||
|
||||
Use `bun.O.RDONLY`, `bun.O.WRONLY | bun.O.CREAT | bun.O.TRUNC`, etc. for open flags.
|
||||
|
||||
### `bun.sys.File` (`src/sys/File.zig`)
|
||||
|
||||
Higher-level file handle wrapping `bun.FileDescriptor`:
|
||||
|
||||
```zig
|
||||
// One-shot read: open + read + close
|
||||
const bytes = switch (bun.sys.File.readFrom(bun.FD.cwd(), path, allocator)) {
|
||||
.result => |b| b,
|
||||
.err => |err| return .{ .err = err },
|
||||
};
|
||||
|
||||
// One-shot write: open + write + close
|
||||
switch (bun.sys.File.writeFile(bun.FD.cwd(), path, data)) {
|
||||
.result => {},
|
||||
.err => |err| return .{ .err = err },
|
||||
}
|
||||
```
|
||||
|
||||
Key methods:
|
||||
|
||||
- `File.open/openat/makeOpen` → `Maybe(File)` (`makeOpen` creates parent dirs)
|
||||
- `file.read/readAll/write/writeAll` — single or looped I/O
|
||||
- `file.readToEnd(allocator)` — read entire file into allocated buffer
|
||||
- `File.readFrom(dir_fd, path, allocator)` — open + read + close
|
||||
- `File.writeFile(dir_fd, path, data)` — open + write + close
|
||||
- `file.stat()`, `file.close()`, `file.writer()`, `file.reader()`
|
||||
|
||||
### `bun.FD` (`src/fd.zig`)
|
||||
|
||||
Cross-platform file descriptor. Use `bun.FD.cwd()` for cwd, `bun.invalid_fd` for sentinel, `fd.close()` to close.
|
||||
|
||||
### `bun.sys.Error` (`src/sys/Error.zig`)
|
||||
|
||||
Preserves errno, syscall tag, and file path. Convert to JS: `err.toSystemError().toErrorInstance(globalObject)`.
|
||||
|
||||
## `bun.strings` — String Utilities (`src/string/immutable.zig`)
|
||||
|
||||
SIMD-accelerated string operations. Use instead of `std.mem` for strings.
|
||||
|
||||
```zig
|
||||
// Searching
|
||||
strings.indexOf(haystack, needle) // ?usize
|
||||
strings.contains(haystack, needle) // bool
|
||||
strings.containsChar(haystack, char) // bool
|
||||
strings.indexOfChar(haystack, char) // ?u32
|
||||
strings.indexOfAny(str, comptime chars) // ?OptionalUsize (SIMD-accelerated)
|
||||
|
||||
// Comparison
|
||||
strings.eql(a, b) // bool
|
||||
strings.eqlComptime(str, comptime literal) // bool — optimized
|
||||
strings.eqlCaseInsensitiveASCII(a, b, comptime true) // 3rd arg = check_len
|
||||
|
||||
// Prefix/Suffix
|
||||
strings.startsWith(str, prefix) // bool
|
||||
strings.endsWith(str, suffix) // bool
|
||||
strings.hasPrefixComptime(str, comptime prefix) // bool — optimized
|
||||
strings.hasSuffixComptime(str, comptime suffix) // bool — optimized
|
||||
|
||||
// Trimming
|
||||
strings.trim(str, comptime chars) // strip from both ends
|
||||
strings.trimSpaces(str) // strip whitespace
|
||||
|
||||
// Encoding conversions
|
||||
strings.toUTF8Alloc(allocator, utf16) // ![]u8
|
||||
strings.toUTF16Alloc(allocator, utf8) // !?[]u16
|
||||
strings.toUTF8FromLatin1(allocator, latin1) // !?Managed(u8)
|
||||
strings.firstNonASCII(slice) // ?u32
|
||||
```
|
||||
|
||||
Bun handles UTF-8, Latin-1, and UTF-16/WTF-16 because JSC uses Latin-1 and UTF-16 internally. Latin-1 is NOT UTF-8 — bytes 128-255 are single chars in Latin-1 but invalid UTF-8.
|
||||
|
||||
### `bun.String` (`src/string.zig`)
|
||||
|
||||
Bridges Zig and JavaScriptCore. Prefer over `ZigString` in new code.
|
||||
|
||||
```zig
|
||||
const s = bun.String.cloneUTF8(utf8_slice); // copies into WTFStringImpl
|
||||
const s = bun.String.borrowUTF8(utf8_slice); // no copy, caller keeps alive
|
||||
const utf8 = s.toUTF8(allocator); // ZigString.Slice
|
||||
defer utf8.deinit();
|
||||
const js_value = s.toJS(globalObject);
|
||||
|
||||
// Create a JS string value directly from UTF-8 bytes:
|
||||
const js_str = try bun.String.createUTF8ForJS(globalObject, utf8_slice);
|
||||
```
|
||||
|
||||
## `bun.path` — Path Manipulation (`src/resolver/resolve_path.zig`)
|
||||
|
||||
Use instead of `std.fs.path`. Platform param: `.auto` (current platform), `.posix`, `.windows`, `.loose` (both separators).
|
||||
|
||||
```zig
|
||||
// Join paths — uses threadlocal buffer, result must be copied if it needs to persist
|
||||
bun.path.join(&.{ dir, filename }, .auto)
|
||||
bun.path.joinZ(&.{ dir, filename }, .auto) // null-terminated
|
||||
|
||||
// Join into a caller-provided buffer
|
||||
bun.path.joinStringBuf(&buf, &.{ a, b }, .auto)
|
||||
bun.path.joinStringBufZ(&buf, &.{ a, b }, .auto) // null-terminated
|
||||
|
||||
// Resolve against an absolute base (like Node.js path.resolve)
|
||||
bun.path.joinAbsString(cwd, &.{ relative_path }, .auto)
|
||||
bun.path.joinAbsStringBufZ(cwd, &buf, &.{ relative_path }, .auto)
|
||||
|
||||
// Path components
|
||||
bun.path.dirname(path, .auto)
|
||||
bun.path.basename(path)
|
||||
|
||||
// Relative path between two absolute paths
|
||||
bun.path.relative(from, to)
|
||||
bun.path.relativeAlloc(allocator, from, to)
|
||||
|
||||
// Normalize (resolve `.` and `..`)
|
||||
bun.path.normalizeBuf(path, &buf, .auto)
|
||||
|
||||
// Null-terminate a path into a buffer
|
||||
bun.path.z(path, &buf) // returns [:0]const u8
|
||||
```
|
||||
|
||||
Use `bun.PathBuffer` for path buffers: `var buf: bun.PathBuffer = undefined;`
|
||||
|
||||
For pooled path buffers (avoids 64KB stack allocations on Windows):
|
||||
|
||||
```zig
|
||||
const buf = bun.path_buffer_pool.get();
|
||||
defer bun.path_buffer_pool.put(buf);
|
||||
```
|
||||
|
||||
## URL Parsing
|
||||
|
||||
Prefer `bun.jsc.URL` (WHATWG-compliant, backed by WebKit C++) over `bun.URL.parse` (internal, doesn't properly handle errors or invalid URLs).
|
||||
|
||||
```zig
|
||||
// Parse a URL string (returns null if invalid)
|
||||
const url = bun.jsc.URL.fromUTF8(href_string) orelse return error.InvalidURL;
|
||||
defer url.deinit();
|
||||
|
||||
url.protocol() // bun.String
|
||||
url.pathname() // bun.String
|
||||
url.search() // bun.String
|
||||
url.hash() // bun.String (includes leading '#')
|
||||
url.port() // u32 (maxInt(u32) if not set, otherwise u16 range)
|
||||
|
||||
// NOTE: host/hostname are SWAPPED vs JS:
|
||||
url.host() // hostname WITHOUT port (opposite of JS!)
|
||||
url.hostname() // hostname WITH port (opposite of JS!)
|
||||
|
||||
// Normalize a URL string (percent-encode, punycode, etc.)
|
||||
const normalized = bun.jsc.URL.hrefFromString(bun.String.borrowUTF8(input));
|
||||
if (normalized.tag == .Dead) return error.InvalidURL;
|
||||
defer normalized.deref();
|
||||
|
||||
// Join base + relative URLs
|
||||
const joined = bun.jsc.URL.join(base_str, relative_str);
|
||||
defer joined.deref();
|
||||
|
||||
// Convert between file paths and file:// URLs
|
||||
const file_url = bun.jsc.URL.fileURLFromString(path_str); // path → file://
|
||||
const file_path = bun.jsc.URL.pathFromFileURL(url_str); // file:// → path
|
||||
```
|
||||
|
||||
## MIME Types (`src/http/MimeType.zig`)
|
||||
|
||||
```zig
|
||||
const MimeType = bun.http.MimeType;
|
||||
|
||||
// Look up by file extension (without leading dot)
|
||||
const mime = MimeType.byExtension("html"); // MimeType{ .value = "text/html", .category = .html }
|
||||
const mime = MimeType.byExtensionNoDefault("xyz"); // ?MimeType (null if unknown)
|
||||
|
||||
// Category checks
|
||||
mime.category // .javascript, .css, .html, .json, .image, .text, .wasm, .font, .video, .audio, ...
|
||||
mime.category.isCode()
|
||||
```
|
||||
|
||||
Common constants: `MimeType.javascript`, `MimeType.json`, `MimeType.html`, `MimeType.css`, `MimeType.text`, `MimeType.wasm`, `MimeType.ico`, `MimeType.other`.
|
||||
|
||||
## Memory & Allocators
|
||||
|
||||
**Use `bun.default_allocator` for almost everything.** It's backed by mimalloc.
|
||||
|
||||
`bun.handleOom(expr)` converts `error.OutOfMemory` into a crash without swallowing other errors:
|
||||
|
||||
```zig
|
||||
const buf = bun.handleOom(allocator.alloc(u8, size)); // correct
|
||||
// NOT: allocator.alloc(u8, size) catch bun.outOfMemory() — could swallow non-OOM errors
|
||||
```
|
||||
|
||||
## Environment Variables (`src/env_var.zig`)
|
||||
|
||||
Type-safe, cached environment variable accessors via `bun.env_var`:
|
||||
|
||||
```zig
|
||||
bun.env_var.HOME.get() // ?[]const u8
|
||||
bun.env_var.CI.get() // ?bool
|
||||
bun.env_var.BUN_CONFIG_DNS_TIME_TO_LIVE_SECONDS.get() // u64 (has default: 30)
|
||||
```
|
||||
|
||||
## Logging (`src/output.zig`)
|
||||
|
||||
```zig
|
||||
const log = bun.Output.scoped(.MY_FEATURE, .visible); // .hidden = opt-in via BUN_DEBUG_MY_FEATURE=1
|
||||
log("processing {d} items", .{count});
|
||||
|
||||
// Color output (convenience wrappers auto-detect TTY):
|
||||
bun.Output.pretty("<green>success<r>: {s}\n", .{msg});
|
||||
bun.Output.prettyErrorln("<red>error<r>: {s}", .{msg});
|
||||
```
|
||||
|
||||
## Spawning Subprocesses (`src/bun.js/api/bun/process.zig`)
|
||||
|
||||
Use `bun.spawnSync` instead of `std.process.Child`:
|
||||
|
||||
```zig
|
||||
switch (bun.spawnSync(&.{
|
||||
.argv = argv,
|
||||
.envp = null, // inherit parent env
|
||||
.cwd = cwd,
|
||||
.stdout = .buffer, // capture
|
||||
.stderr = .inherit, // pass through
|
||||
.stdin = .ignore,
|
||||
|
||||
.windows = if (bun.Environment.isWindows) .{
|
||||
.loop = bun.jsc.EventLoopHandle.init(bun.jsc.MiniEventLoop.initGlobal(env, null)),
|
||||
},
|
||||
}) catch return) {
|
||||
.err => |err| { /* bun.sys.Error */ },
|
||||
.result => |result| {
|
||||
defer result.deinit();
|
||||
const stdout = result.stdout.items;
|
||||
if (result.status.isOK()) { ... }
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Options: `argv: []const []const u8`, `envp: ?[*:null]?[*:0]const u8` (null = inherit), `argv0: ?[*:0]const u8`. Stdio: `.inherit`, `.ignore`, `.buffer`.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
```zig
|
||||
// Read a file
|
||||
const contents = switch (bun.sys.File.readFrom(bun.FD.cwd(), path, allocator)) {
|
||||
.result => |bytes| bytes,
|
||||
.err => |err| { globalObject.throwValue(err.toSystemError().toErrorInstance(globalObject)); return .zero; },
|
||||
};
|
||||
|
||||
// Create directories recursively
|
||||
bun.makePath(dir.stdDir(), sub_path) catch |err| { ... };
|
||||
|
||||
// Hashing
|
||||
bun.hash(bytes) // u64 — wyhash
|
||||
bun.hash32(bytes) // u32
|
||||
```
|
||||
|
||||
@@ -95,6 +95,7 @@
|
||||
#include <JavaScriptCore/RegExp.h>
|
||||
#include <JavaScriptCore/RegExpObject.h>
|
||||
#include <JavaScriptCore/TypedArrayInlines.h>
|
||||
#include <JavaScriptCore/TypedArrayType.h>
|
||||
#include <JavaScriptCore/TypedArrays.h>
|
||||
#include <JavaScriptCore/WasmModule.h>
|
||||
#include <JavaScriptCore/YarrFlags.h>
|
||||
@@ -385,6 +386,38 @@ static unsigned typedArrayElementSize(ArrayBufferViewSubtag tag)
|
||||
}
|
||||
}
|
||||
|
||||
static ArrayBufferViewSubtag subtagForTypedArrayType(TypedArrayType type)
|
||||
{
|
||||
switch (type) {
|
||||
case TypeInt8:
|
||||
return Int8ArrayTag;
|
||||
case TypeUint8:
|
||||
return Uint8ArrayTag;
|
||||
case TypeUint8Clamped:
|
||||
return Uint8ClampedArrayTag;
|
||||
case TypeInt16:
|
||||
return Int16ArrayTag;
|
||||
case TypeUint16:
|
||||
return Uint16ArrayTag;
|
||||
case TypeInt32:
|
||||
return Int32ArrayTag;
|
||||
case TypeUint32:
|
||||
return Uint32ArrayTag;
|
||||
case TypeFloat16:
|
||||
return Float16ArrayTag;
|
||||
case TypeFloat32:
|
||||
return Float32ArrayTag;
|
||||
case TypeFloat64:
|
||||
return Float64ArrayTag;
|
||||
case TypeBigInt64:
|
||||
return BigInt64ArrayTag;
|
||||
case TypeBigUint64:
|
||||
return BigUint64ArrayTag;
|
||||
default:
|
||||
return DataViewTag;
|
||||
}
|
||||
}
|
||||
|
||||
enum class SerializableErrorType : uint8_t {
|
||||
Error,
|
||||
EvalError,
|
||||
@@ -5606,6 +5639,14 @@ SerializedScriptValue::SerializedScriptValue(WTF::FixedVector<DenseArrayElement>
|
||||
m_memoryCost = computeMemoryCost();
|
||||
}
|
||||
|
||||
SerializedScriptValue::SerializedScriptValue(Vector<uint8_t>&& data, uint8_t subtag)
|
||||
: m_arrayButterflyData(WTF::move(data))
|
||||
, m_fastPath(FastPath::TypedArray)
|
||||
, m_typedArraySubtag(subtag)
|
||||
{
|
||||
m_memoryCost = computeMemoryCost();
|
||||
}
|
||||
|
||||
Ref<SerializedScriptValue> SerializedScriptValue::createDenseArrayFastPath(
|
||||
WTF::FixedVector<DenseArrayElement>&& elements)
|
||||
{
|
||||
@@ -5695,6 +5736,7 @@ size_t SerializedScriptValue::computeMemoryCost() const
|
||||
break;
|
||||
case FastPath::Int32Array:
|
||||
case FastPath::DoubleArray:
|
||||
case FastPath::TypedArray:
|
||||
cost += m_arrayButterflyData.size();
|
||||
break;
|
||||
case FastPath::DenseArray:
|
||||
@@ -5916,7 +5958,35 @@ ExceptionOr<Ref<SerializedScriptValue>> SerializedScriptValue::create(JSGlobalOb
|
||||
object = cell->getObject();
|
||||
structure = object->structure();
|
||||
|
||||
if (auto* jsArray = jsDynamicCast<JSArray*>(object)) {
|
||||
// TypedArray fast path: check before JSArray since TypedArray is not a JSArray
|
||||
auto jsType = structure->typeInfo().type();
|
||||
if (isTypedView(jsType)) {
|
||||
auto* view = jsCast<JSArrayBufferView*>(object);
|
||||
size_t byteLength = view->byteLength();
|
||||
if (!view->isDetached()
|
||||
&& !view->isOutOfBounds()
|
||||
&& !view->isShared()
|
||||
&& !view->isResizableOrGrowableShared()
|
||||
&& view->byteOffset() == 0
|
||||
&& structure->maxOffset() == invalidOffset
|
||||
// For WastefulTypedArray (hasArrayBuffer()==true), verify the view
|
||||
// covers the full ArrayBuffer; partial views (e.g. new Uint8Array(buf, 0, 8)
|
||||
// over a 16-byte buffer) must fall through to the slow path.
|
||||
// possiblySharedBuffer() is safe after isDetached()/isShared() checks:
|
||||
// WastefulTypedArray just returns existingBufferInButterfly() without
|
||||
// triggering slowDownAndWasteMemory(). Must be evaluated AFTER isDetached()
|
||||
// to avoid null deref on detached buffers.
|
||||
&& (!view->hasArrayBuffer()
|
||||
|| view->byteLength() == view->possiblySharedBuffer()->byteLength())) {
|
||||
auto taType = typedArrayType(jsType);
|
||||
auto subtag = subtagForTypedArrayType(taType);
|
||||
auto* data = static_cast<const uint8_t*>(view->vector());
|
||||
// Use span constructor: single allocation + memcpy, no zero-fill
|
||||
Vector<uint8_t> buffer(std::span<const uint8_t> { data, byteLength });
|
||||
return SerializedScriptValue::createTypedArrayFastPath(WTF::move(buffer), static_cast<uint8_t>(subtag));
|
||||
}
|
||||
// Conditions not met → fall through to slow path
|
||||
} else if (auto* jsArray = jsDynamicCast<JSArray*>(object)) {
|
||||
canUseArrayFastPath = true;
|
||||
array = jsArray;
|
||||
} else if (isObjectFastPathCandidate(structure)) {
|
||||
@@ -6366,6 +6436,11 @@ Ref<SerializedScriptValue> SerializedScriptValue::createDoubleArrayFastPath(Vect
|
||||
return adoptRef(*new SerializedScriptValue(WTF::move(data), length, FastPath::DoubleArray));
|
||||
}
|
||||
|
||||
Ref<SerializedScriptValue> SerializedScriptValue::createTypedArrayFastPath(Vector<uint8_t>&& data, uint8_t subtag)
|
||||
{
|
||||
return adoptRef(*new SerializedScriptValue(WTF::move(data), subtag));
|
||||
}
|
||||
|
||||
RefPtr<SerializedScriptValue> SerializedScriptValue::create(JSContextRef originContext, JSValueRef apiValue, JSValueRef* exception)
|
||||
{
|
||||
JSGlobalObject* lexicalGlobalObject = toJS(originContext);
|
||||
@@ -6676,6 +6751,71 @@ JSValue SerializedScriptValue::deserialize(JSGlobalObject& lexicalGlobalObject,
|
||||
*didFail = false;
|
||||
return resultArray;
|
||||
}
|
||||
case FastPath::TypedArray: {
|
||||
size_t byteLength = m_arrayButterflyData.size();
|
||||
auto subtag = static_cast<ArrayBufferViewSubtag>(m_typedArraySubtag);
|
||||
unsigned elemSize = typedArrayElementSize(subtag);
|
||||
if (!elemSize) [[unlikely]]
|
||||
break;
|
||||
|
||||
auto arrayBuffer = ArrayBuffer::tryCreate(m_arrayButterflyData.span());
|
||||
if (!arrayBuffer) [[unlikely]] {
|
||||
if (didFail)
|
||||
*didFail = true;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::optional<size_t> length = byteLength / elemSize;
|
||||
JSValue typedArrayValue;
|
||||
|
||||
switch (subtag) {
|
||||
case Int8ArrayTag:
|
||||
typedArrayValue = toJS(&lexicalGlobalObject, globalObject, Int8Array::wrappedAs(arrayBuffer.releaseNonNull(), 0, length).get());
|
||||
break;
|
||||
case Uint8ArrayTag:
|
||||
typedArrayValue = toJS(&lexicalGlobalObject, globalObject, Uint8Array::wrappedAs(arrayBuffer.releaseNonNull(), 0, length).get());
|
||||
break;
|
||||
case Uint8ClampedArrayTag:
|
||||
typedArrayValue = toJS(&lexicalGlobalObject, globalObject, Uint8ClampedArray::wrappedAs(arrayBuffer.releaseNonNull(), 0, length).get());
|
||||
break;
|
||||
case Int16ArrayTag:
|
||||
typedArrayValue = toJS(&lexicalGlobalObject, globalObject, Int16Array::wrappedAs(arrayBuffer.releaseNonNull(), 0, length).get());
|
||||
break;
|
||||
case Uint16ArrayTag:
|
||||
typedArrayValue = toJS(&lexicalGlobalObject, globalObject, Uint16Array::wrappedAs(arrayBuffer.releaseNonNull(), 0, length).get());
|
||||
break;
|
||||
case Int32ArrayTag:
|
||||
typedArrayValue = toJS(&lexicalGlobalObject, globalObject, Int32Array::wrappedAs(arrayBuffer.releaseNonNull(), 0, length).get());
|
||||
break;
|
||||
case Uint32ArrayTag:
|
||||
typedArrayValue = toJS(&lexicalGlobalObject, globalObject, Uint32Array::wrappedAs(arrayBuffer.releaseNonNull(), 0, length).get());
|
||||
break;
|
||||
case Float16ArrayTag:
|
||||
typedArrayValue = toJS(&lexicalGlobalObject, globalObject, Float16Array::wrappedAs(arrayBuffer.releaseNonNull(), 0, length).get());
|
||||
break;
|
||||
case Float32ArrayTag:
|
||||
typedArrayValue = toJS(&lexicalGlobalObject, globalObject, Float32Array::wrappedAs(arrayBuffer.releaseNonNull(), 0, length).get());
|
||||
break;
|
||||
case Float64ArrayTag:
|
||||
typedArrayValue = toJS(&lexicalGlobalObject, globalObject, Float64Array::wrappedAs(arrayBuffer.releaseNonNull(), 0, length).get());
|
||||
break;
|
||||
case BigInt64ArrayTag:
|
||||
typedArrayValue = toJS(&lexicalGlobalObject, globalObject, BigInt64Array::wrappedAs(arrayBuffer.releaseNonNull(), 0, length).get());
|
||||
break;
|
||||
case BigUint64ArrayTag:
|
||||
typedArrayValue = toJS(&lexicalGlobalObject, globalObject, BigUint64Array::wrappedAs(arrayBuffer.releaseNonNull(), 0, length).get());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (typedArrayValue) {
|
||||
if (didFail)
|
||||
*didFail = false;
|
||||
return typedArrayValue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FastPath::None: {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ enum class FastPath : uint8_t {
|
||||
Int32Array,
|
||||
DoubleArray,
|
||||
DenseArray,
|
||||
TypedArray,
|
||||
};
|
||||
|
||||
#if ENABLE(OFFSCREEN_CANVAS_IN_WORKERS)
|
||||
@@ -148,6 +149,9 @@ public:
|
||||
// Fast path for postMessage with dense arrays containing simple objects
|
||||
static Ref<SerializedScriptValue> createDenseArrayFastPath(WTF::FixedVector<DenseArrayElement>&& elements);
|
||||
|
||||
// Fast path for postMessage with TypedArray (Uint8Array, Float64Array, etc.)
|
||||
static Ref<SerializedScriptValue> createTypedArrayFastPath(Vector<uint8_t>&& data, uint8_t subtag);
|
||||
|
||||
static Ref<SerializedScriptValue> nullValue();
|
||||
|
||||
WEBCORE_EXPORT JSC::JSValue deserialize(JSC::JSGlobalObject&, JSC::JSGlobalObject*, SerializationErrorMode = SerializationErrorMode::Throwing, bool* didFail = nullptr);
|
||||
@@ -255,6 +259,8 @@ private:
|
||||
SerializedScriptValue(Vector<uint8_t>&& butterflyData, uint32_t length, FastPath fastPath);
|
||||
// Constructor for DenseArray fast path
|
||||
explicit SerializedScriptValue(WTF::FixedVector<DenseArrayElement>&& denseElements);
|
||||
// Constructor for TypedArray fast path
|
||||
SerializedScriptValue(Vector<uint8_t>&& data, uint8_t subtag);
|
||||
|
||||
size_t computeMemoryCost() const;
|
||||
|
||||
@@ -294,6 +300,9 @@ private:
|
||||
|
||||
// DenseArray fast path: array of primitives/strings/simple objects
|
||||
FixedVector<DenseArrayElement> m_denseArrayElements {};
|
||||
|
||||
// TypedArray fast path: subtag identifying the TypedArray type (ArrayBufferViewSubtag)
|
||||
uint8_t m_typedArraySubtag { 0 };
|
||||
};
|
||||
|
||||
template<class Encoder>
|
||||
|
||||
@@ -222,7 +222,7 @@ pub const Linker = struct {
|
||||
|
||||
if (comptime is_bun) {
|
||||
// make these happen at runtime
|
||||
if (import_record.kind == .require or import_record.kind == .require_resolve) {
|
||||
if (import_record.kind == .require or import_record.kind == .require_resolve or import_record.kind == .dynamic) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -927,7 +927,9 @@ pub const Log = struct {
|
||||
err: anyerror,
|
||||
) OOM!void {
|
||||
@branchHint(.cold);
|
||||
return try addResolveErrorWithLevel(log, source, r, allocator, fmt, args, import_kind, false, .err, err);
|
||||
// Always dupe the line_text from the source to ensure the Location data
|
||||
// outlives the source's backing memory (which may be arena-allocated).
|
||||
return try addResolveErrorWithLevel(log, source, r, allocator, fmt, args, import_kind, true, .err, err);
|
||||
}
|
||||
|
||||
pub fn addResolveErrorWithTextDupe(
|
||||
|
||||
@@ -1165,7 +1165,7 @@ const CanonicalRequest = struct {
|
||||
/// Returns true if the given slice contains any CR (\r) or LF (\n) characters,
|
||||
/// which would allow HTTP header injection if used in a header value.
|
||||
fn containsNewlineOrCR(value: []const u8) bool {
|
||||
return std.mem.indexOfAny(u8, value, "\r\n") != null;
|
||||
return strings.indexOfAny(value, "\r\n") != null;
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
@@ -883,4 +883,253 @@ describe("Structured Clone Fast Path", () => {
|
||||
port1.close();
|
||||
port2.close();
|
||||
});
|
||||
|
||||
// === TypedArray fast path tests ===
|
||||
|
||||
const typedArrayCtors = [
|
||||
{ name: "Uint8Array", ctor: Uint8Array, values: [0, 1, 127, 255] },
|
||||
{ name: "Int8Array", ctor: Int8Array, values: [-128, -1, 0, 1, 127] },
|
||||
{ name: "Uint8ClampedArray", ctor: Uint8ClampedArray, values: [0, 1, 127, 255] },
|
||||
{ name: "Uint16Array", ctor: Uint16Array, values: [0, 1, 256, 65535] },
|
||||
{ name: "Int16Array", ctor: Int16Array, values: [-32768, -1, 0, 1, 32767] },
|
||||
{ name: "Uint32Array", ctor: Uint32Array, values: [0, 1, 65536, 4294967295] },
|
||||
{ name: "Int32Array", ctor: Int32Array, values: [-2147483648, -1, 0, 1, 2147483647] },
|
||||
{ name: "Float32Array", ctor: Float32Array, values: [0, 1.5, -1.5, 3.4028234663852886e38] },
|
||||
{ name: "Float64Array", ctor: Float64Array, values: [0, 1.5, -1.5, Number.MAX_VALUE, Number.MIN_VALUE] },
|
||||
] as const;
|
||||
|
||||
for (const { name, ctor, values } of typedArrayCtors) {
|
||||
test(`structuredClone(${name}) basic values`, () => {
|
||||
const input = new ctor(values as any);
|
||||
const cloned = structuredClone(input);
|
||||
expect(cloned).toBeInstanceOf(ctor);
|
||||
expect(cloned).toEqual(input);
|
||||
expect(cloned.buffer).not.toBe(input.buffer);
|
||||
});
|
||||
}
|
||||
|
||||
test("structuredClone(BigInt64Array) basic values", () => {
|
||||
const input = new BigInt64Array([-9223372036854775808n, -1n, 0n, 1n, 9223372036854775807n]);
|
||||
const cloned = structuredClone(input);
|
||||
expect(cloned).toBeInstanceOf(BigInt64Array);
|
||||
expect(cloned).toEqual(input);
|
||||
expect(cloned.buffer).not.toBe(input.buffer);
|
||||
});
|
||||
|
||||
test("structuredClone(BigUint64Array) basic values", () => {
|
||||
const input = new BigUint64Array([0n, 1n, 18446744073709551615n]);
|
||||
const cloned = structuredClone(input);
|
||||
expect(cloned).toBeInstanceOf(BigUint64Array);
|
||||
expect(cloned).toEqual(input);
|
||||
expect(cloned.buffer).not.toBe(input.buffer);
|
||||
});
|
||||
|
||||
test("structuredClone(Float16Array) basic values", () => {
|
||||
const input = new Float16Array([0, 1.5, -1.5, 65504]);
|
||||
const cloned = structuredClone(input);
|
||||
expect(cloned).toBeInstanceOf(Float16Array);
|
||||
expect(cloned).toEqual(input);
|
||||
expect(cloned.buffer).not.toBe(input.buffer);
|
||||
});
|
||||
|
||||
test("structuredClone empty TypedArray", () => {
|
||||
const input = new Uint8Array(0);
|
||||
const cloned = structuredClone(input);
|
||||
expect(cloned).toBeInstanceOf(Uint8Array);
|
||||
expect(cloned.length).toBe(0);
|
||||
expect(cloned.byteLength).toBe(0);
|
||||
});
|
||||
|
||||
test("structuredClone large TypedArray (1MB)", () => {
|
||||
const input = new Uint8Array(1024 * 1024);
|
||||
for (let i = 0; i < input.length; i++) input[i] = i & 0xff;
|
||||
const cloned = structuredClone(input);
|
||||
expect(cloned).toBeInstanceOf(Uint8Array);
|
||||
expect(cloned.length).toBe(input.length);
|
||||
expect(cloned).toEqual(input);
|
||||
expect(cloned.buffer).not.toBe(input.buffer);
|
||||
});
|
||||
|
||||
test("structuredClone Float64Array with special values", () => {
|
||||
const input = new Float64Array([NaN, Infinity, -Infinity, -0, 0]);
|
||||
const cloned = structuredClone(input);
|
||||
expect(cloned).toBeInstanceOf(Float64Array);
|
||||
expect(cloned[0]).toBeNaN();
|
||||
expect(cloned[1]).toBe(Infinity);
|
||||
expect(cloned[2]).toBe(-Infinity);
|
||||
expect(Object.is(cloned[3], -0)).toBe(true);
|
||||
expect(cloned[4]).toBe(0);
|
||||
});
|
||||
|
||||
test("structuredClone Float32Array with special values", () => {
|
||||
const input = new Float32Array([NaN, Infinity, -Infinity, -0]);
|
||||
const cloned = structuredClone(input);
|
||||
expect(cloned).toBeInstanceOf(Float32Array);
|
||||
expect(cloned[0]).toBeNaN();
|
||||
expect(cloned[1]).toBe(Infinity);
|
||||
expect(cloned[2]).toBe(-Infinity);
|
||||
expect(Object.is(cloned[3], -0)).toBe(true);
|
||||
});
|
||||
|
||||
test("structuredClone TypedArray creates independent copy", () => {
|
||||
const input = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const cloned = structuredClone(input);
|
||||
cloned[0] = 255;
|
||||
expect(input[0]).toBe(1);
|
||||
input[1] = 200;
|
||||
expect(cloned[1]).toBe(2);
|
||||
});
|
||||
|
||||
test("structuredClone DataView falls back to slow path but works correctly", () => {
|
||||
const buf = new ArrayBuffer(8);
|
||||
const view = new DataView(buf);
|
||||
view.setFloat64(0, 3.14);
|
||||
const cloned = structuredClone(view);
|
||||
expect(cloned).toBeInstanceOf(DataView);
|
||||
expect(cloned.getFloat64(0)).toBe(3.14);
|
||||
expect(cloned.buffer).not.toBe(buf);
|
||||
});
|
||||
|
||||
test("structuredClone TypedArray slice view falls back to slow path", () => {
|
||||
const buf = new ArrayBuffer(16);
|
||||
new Uint8Array(buf).set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
|
||||
const sliceView = new Uint8Array(buf, 4, 4);
|
||||
const cloned = structuredClone(sliceView);
|
||||
expect(cloned).toBeInstanceOf(Uint8Array);
|
||||
expect(cloned).toEqual(new Uint8Array([4, 5, 6, 7]));
|
||||
// structuredClone clones the full backing ArrayBuffer, preserving byteOffset
|
||||
expect(cloned.byteOffset).toBe(4);
|
||||
expect(cloned.buffer.byteLength).toBe(16);
|
||||
});
|
||||
|
||||
test("structuredClone TypedArray with named properties falls back to slow path", () => {
|
||||
const input = new Uint8Array([1, 2, 3]) as any;
|
||||
input.customProp = "hello";
|
||||
// Named properties on TypedArray are not cloneable via structuredClone,
|
||||
// the slow path handles this correctly (ignores them)
|
||||
const cloned = structuredClone(input);
|
||||
expect(cloned).toBeInstanceOf(Uint8Array);
|
||||
expect(cloned).toEqual(new Uint8Array([1, 2, 3]));
|
||||
});
|
||||
|
||||
test("structuredClone detached TypedArray throws DataCloneError", () => {
|
||||
const buf = new ArrayBuffer(8);
|
||||
const input = new Uint8Array(buf);
|
||||
// Detach the buffer by transferring it
|
||||
structuredClone(buf, { transfer: [buf] });
|
||||
expect(() => structuredClone(input)).toThrow();
|
||||
});
|
||||
|
||||
test("postMessage TypedArray via MessageChannel", async () => {
|
||||
const { port1, port2 } = new MessageChannel();
|
||||
const input = new Uint8Array([10, 20, 30, 40, 50]);
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
port2.onmessage = (e: MessageEvent) => resolve(e.data);
|
||||
port1.postMessage(input);
|
||||
const result = await promise;
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result).toEqual(input);
|
||||
port1.close();
|
||||
port2.close();
|
||||
});
|
||||
|
||||
test("postMessage Float64Array via MessageChannel", async () => {
|
||||
const { port1, port2 } = new MessageChannel();
|
||||
const input = new Float64Array([1.1, 2.2, 3.3, NaN, Infinity]);
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
port2.onmessage = (e: MessageEvent) => resolve(e.data);
|
||||
port1.postMessage(input);
|
||||
const result = await promise;
|
||||
expect(result).toBeInstanceOf(Float64Array);
|
||||
expect(result[0]).toBe(1.1);
|
||||
expect(result[1]).toBe(2.2);
|
||||
expect(result[2]).toBe(3.3);
|
||||
expect(result[3]).toBeNaN();
|
||||
expect(result[4]).toBe(Infinity);
|
||||
port1.close();
|
||||
port2.close();
|
||||
});
|
||||
|
||||
test("postMessage BigInt64Array via MessageChannel", async () => {
|
||||
const { port1, port2 } = new MessageChannel();
|
||||
const input = new BigInt64Array([0n, -1n, 9223372036854775807n]);
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
port2.onmessage = (e: MessageEvent) => resolve(e.data);
|
||||
port1.postMessage(input);
|
||||
const result = await promise;
|
||||
expect(result).toBeInstanceOf(BigInt64Array);
|
||||
expect(result).toEqual(input);
|
||||
port1.close();
|
||||
port2.close();
|
||||
});
|
||||
|
||||
test("structuredClone TypedArray backed by SharedArrayBuffer falls back to slow path", () => {
|
||||
const sab = new SharedArrayBuffer(16);
|
||||
const view = new Uint8Array(sab);
|
||||
view.set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
|
||||
const cloned = structuredClone(view);
|
||||
expect(cloned).toBeInstanceOf(Uint8Array);
|
||||
expect(cloned).toEqual(view);
|
||||
// The cloned view should NOT share memory with the original
|
||||
expect(cloned.buffer).not.toBe(sab);
|
||||
// Verify independence: modifying original doesn't affect clone
|
||||
view[0] = 255;
|
||||
expect(cloned[0]).toBe(1);
|
||||
});
|
||||
|
||||
test("structuredClone Int32Array backed by SharedArrayBuffer preserves values", () => {
|
||||
const sab = new SharedArrayBuffer(16);
|
||||
const view = new Int32Array(sab);
|
||||
view.set([100, 200, 300, 400]);
|
||||
const cloned = structuredClone(view);
|
||||
expect(cloned).toBeInstanceOf(Int32Array);
|
||||
expect(cloned).toEqual(new Int32Array([100, 200, 300, 400]));
|
||||
expect(cloned.buffer).not.toBe(sab);
|
||||
});
|
||||
|
||||
test("postMessage TypedArray backed by SharedArrayBuffer via MessageChannel", async () => {
|
||||
const { port1, port2 } = new MessageChannel();
|
||||
const sab = new SharedArrayBuffer(8);
|
||||
const input = new Uint8Array(sab);
|
||||
input.set([10, 20, 30, 40, 50, 60, 70, 80]);
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
port2.onmessage = (e: MessageEvent) => resolve(e.data);
|
||||
port1.postMessage(input);
|
||||
const result = await promise;
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result).toEqual(input);
|
||||
port1.close();
|
||||
port2.close();
|
||||
});
|
||||
|
||||
test("structuredClone partial-buffer TypedArray with byteOffset==0 preserves full buffer", () => {
|
||||
// new Uint8Array(buf, 0, 8) over a 16-byte buffer: byteOffset is 0 but
|
||||
// the view only covers the first half. The slow path clones the entire
|
||||
// backing ArrayBuffer and preserves byteOffset/byteLength.
|
||||
const buf = new ArrayBuffer(16);
|
||||
const full = new Uint8Array(buf);
|
||||
full.set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
|
||||
const partial = new Uint8Array(buf, 0, 8);
|
||||
|
||||
const cloned = structuredClone(partial);
|
||||
expect(cloned).toBeInstanceOf(Uint8Array);
|
||||
expect(cloned).toEqual(new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]));
|
||||
expect(cloned.byteOffset).toBe(0);
|
||||
expect(cloned.byteLength).toBe(8);
|
||||
// The cloned buffer must preserve the full backing ArrayBuffer size
|
||||
expect(cloned.buffer.byteLength).toBe(16);
|
||||
});
|
||||
|
||||
test("structuredClone partial-buffer Int32Array with byteOffset==0 preserves full buffer", () => {
|
||||
const buf = new ArrayBuffer(32);
|
||||
const partial = new Int32Array(buf, 0, 4); // 16 bytes out of 32
|
||||
partial.set([100, 200, 300, 400]);
|
||||
|
||||
const cloned = structuredClone(partial);
|
||||
expect(cloned).toBeInstanceOf(Int32Array);
|
||||
expect(cloned).toEqual(new Int32Array([100, 200, 300, 400]));
|
||||
expect(cloned.byteOffset).toBe(0);
|
||||
expect(cloned.byteLength).toBe(16);
|
||||
expect(cloned.buffer.byteLength).toBe(32);
|
||||
});
|
||||
});
|
||||
|
||||
113
test/regression/issue/25707.test.ts
Normal file
113
test/regression/issue/25707.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDir } from "harness";
|
||||
|
||||
// https://github.com/oven-sh/bun/issues/25707
|
||||
// Dynamic import() of non-existent node: modules inside CJS files should not
|
||||
// fail at transpile/require time. They should be deferred to runtime so that
|
||||
// try/catch can handle the error gracefully.
|
||||
|
||||
test("require() of CJS file containing dynamic import of non-existent node: module does not fail at load time", async () => {
|
||||
using dir = tempDir("issue-25707", {
|
||||
// Simulates turbopack-generated chunks: a CJS module with a factory function
|
||||
// containing import("node:sqlite") inside a try/catch that is never called
|
||||
// during require().
|
||||
"chunk.js": `
|
||||
module.exports = [
|
||||
function factory(exports) {
|
||||
async function detect(e) {
|
||||
if ("createSession" in e) {
|
||||
let c;
|
||||
try {
|
||||
({DatabaseSync: c} = await import("node:sqlite"))
|
||||
} catch(a) {
|
||||
if (null !== a && "object" == typeof a && "code" in a && "ERR_UNKNOWN_BUILTIN_MODULE" !== a.code)
|
||||
throw a;
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.detect = detect;
|
||||
}
|
||||
];
|
||||
`,
|
||||
"main.js": `
|
||||
// This require() should not fail even though chunk.js contains import("node:sqlite")
|
||||
const factories = require("./chunk.js");
|
||||
console.log("loaded " + factories.length + " factories");
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "main.js"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout.trim()).toBe("loaded 1 factories");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("require() of CJS file with bare dynamic import of non-existent node: module does not fail at load time", async () => {
|
||||
// The dynamic import is NOT inside a try/catch, but it's still a dynamic import
|
||||
// that should only be resolved at runtime, not at transpile time
|
||||
using dir = tempDir("issue-25707-bare", {
|
||||
"lib.js": `
|
||||
module.exports = async function() {
|
||||
const { DatabaseSync } = await import("node:sqlite");
|
||||
return DatabaseSync;
|
||||
};
|
||||
`,
|
||||
"main.js": `
|
||||
const fn = require("./lib.js");
|
||||
console.log("loaded");
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "main.js"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout.trim()).toBe("loaded");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("dynamic import of non-existent node: module in CJS rejects at runtime with correct error", async () => {
|
||||
using dir = tempDir("issue-25707-runtime", {
|
||||
"lib.js": `
|
||||
module.exports = async function() {
|
||||
try {
|
||||
const { DatabaseSync } = await import("node:sqlite");
|
||||
return "resolved";
|
||||
} catch (e) {
|
||||
return "caught: " + e.code;
|
||||
}
|
||||
};
|
||||
`,
|
||||
"main.js": `
|
||||
const fn = require("./lib.js");
|
||||
fn().then(result => console.log(result));
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "main.js"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout.trim()).toBe("caught: ERR_UNKNOWN_BUILTIN_MODULE");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
Reference in New Issue
Block a user