Compare commits

..

6 Commits

Author SHA1 Message Date
Claude Bot
2cc7a58e77 Add documentation for publicHoistPattern and hoistPattern
Documents the publicHoistPattern and hoistPattern configuration options
for bunfig.toml. These settings control dependency hoisting behavior when
using isolated installs (linker = "isolated").

- publicHoistPattern: Controls hoisting to root node_modules
- hoistPattern: Controls hoisting within each package's node_modules

Both settings are pnpm-compatible and match against dependency names only.
Includes examples for common use cases and .npmrc configuration syntax.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-31 19:33:51 +00:00
Jarred Sumner
a5f8b0e8dd react-dom-server requires messagechannel now i guess 2025-10-29 08:14:08 +01:00
Jarred Sumner
fe1bc56637 Add workerd benchmark 2025-10-29 07:16:32 +01:00
robobun
98c04e37ec Fix source index bounds check in sourcemap decoder (#24145)
## Summary

Fix the source index bounds check in `src/sourcemap/Mapping.zig` to
correctly validate indices against the range `[0, sources_count)`.

## Changes

- Changed the bounds check condition from `source_index > sources_count`
to `source_index >= sources_count` on line 452
- This prevents accepting `source_index == sources_count`, which would
be out of bounds when indexing into the sources array

## Test plan

- [x] Built successfully with `bun bd`
- The existing test suite should continue to pass

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

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-28 12:32:53 -07:00
robobun
4f1b90ad1d Fix EventEmitter crash in removeAllListeners with removeListener meta-listener (#24148)
## Summary

Fixes #24147

- Fixed EventEmitter crash when `removeAllListeners()` is called from
within an event handler while a `removeListener` meta-listener is
registered
- Added undefined check before iterating over listeners array to match
Node.js behavior
- Added comprehensive regression tests

## Bug Description

When `removeAllListeners(type)` was called:
1. From within an event handler 
2. While a `removeListener` meta-listener was registered
3. For an event type with no listeners

It would crash with: `TypeError: undefined is not an object (evaluating
'this._events')`

## Root Cause

The `removeAllListeners` function tried to access `listeners.length`
without checking if `listeners` was defined first. When called with an
event type that had no listeners, `events[type]` returned `undefined`,
causing the crash.

## Fix

Added a check `if (listeners !== undefined)` before iterating, matching
the behavior in Node.js core:
https://github.com/nodejs/node/blob/main/lib/events.js#L768

## Test plan

-  Created regression test in `test/regression/issue/24147.test.ts`
-  Verified test fails with `USE_SYSTEM_BUN=1 bun test` (reproduces
bug)
-  Verified test passes with `bun bd test` (confirms fix)
-  Test covers the exact reproduction case from the issue
-  Additional tests for edge cases (actual listeners, nested calls)

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

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-28 12:32:15 -07:00
robobun
51431b6e65 Fix sourcemap comparator to use strict weak ordering (#24146)
## Summary

Fixes the comparator function in `src/sourcemap/Mapping.zig` to use
strict weak ordering as required by sort algorithms.

## Changes

- Changed `<=` to `<` in the column comparison to ensure strict ordering
- Refactored the comparator to use clearer if-statement structure
- Added index comparison as a tiebreaker for stable sorting when both
line and column positions are equal

## Problem

The original comparator used `<=` which would return true for equal
elements, violating the strict weak ordering requirement. This could
lead to undefined behavior in sorting.

**Before:**
```zig
return a.lines.zeroBased() < b.lines.zeroBased() or (a.lines.zeroBased() == b.lines.zeroBased() and a.columns.zeroBased() <= b.columns.zeroBased());
```

**After:**
```zig
if (a.lines.zeroBased() != b.lines.zeroBased()) {
    return a.lines.zeroBased() < b.lines.zeroBased();
}
if (a.columns.zeroBased() != b.columns.zeroBased()) {
    return a.columns.zeroBased() < b.columns.zeroBased();
}
return a_index < b_index;
```

## Test plan

- [x] Verified compilation with `bun bd`
- The sort now properly follows strict weak ordering semantics

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

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-28 12:31:42 -07:00
11 changed files with 334 additions and 76 deletions

View File

@@ -4,20 +4,16 @@
"": {
"name": "react-hello-world",
"dependencies": {
"react": "next",
"react-dom": "next",
"react": "^19.2.0",
"react-dom": "^19.2.0",
},
},
},
"packages": {
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
"react": ["react@18.3.0-next-b72ed698f-20230303", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-l6RbwXa9Peerh9pQEq62DDypxSQfavbybY0wV1vwZ63X0P5VaaEesZAz1KPpnVvXjTtQaOMQsIPvnQwmaVqzTQ=="],
"react-dom": ["react-dom@18.3.0-next-b72ed698f-20230303", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "0.24.0-next-b72ed698f-20230303" }, "peerDependencies": { "react": "18.3.0-next-b72ed698f-20230303" } }, "sha512-0Gh/gmTT6H8KxswIQB/8shdTTfs6QIu86nNqZf3Y0RBqIwgTVxRaQVz14/Fw4/Nt81nK/Jt6KT4bx3yvOxZDGQ=="],
"scheduler": ["scheduler@0.24.0-next-b72ed698f-20230303", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-ct4DMMFbc2kFxCdvbG+i/Jn1S1oqrIFSn2VX/mam+Ya0iuNy+lb8rgT7A+YBUqrQNDaNEqABYI2sOQgqoRxp7w=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
}
}

View File

@@ -4,13 +4,14 @@
"description": "",
"main": "react-hello-world.node.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"build:workerd": "bun build react-hello-world.workerd.jsx --outfile=react-hello-world.workerd.js --format=esm --production && (echo '// MessageChannel polyfill for workerd'; echo 'if (typeof MessageChannel === \"undefined\") {'; echo ' globalThis.MessageChannel = class MessageChannel {'; echo ' constructor() {'; echo ' this.port1 = { onmessage: null, postMessage: () => {} };'; echo ' this.port2 = {'; echo ' postMessage: (msg) => {'; echo ' if (this.port1.onmessage) {'; echo ' queueMicrotask(() => this.port1.onmessage({ data: msg }));'; echo ' }'; echo ' }'; echo ' };'; echo ' }'; echo ' };'; echo '}'; cat react-hello-world.workerd.js) > temp.js && mv temp.js react-hello-world.workerd.js"
},
"keywords": [],
"author": "Colin McDonnell",
"license": "ISC",
"dependencies": {
"react": "next",
"react-dom": "next"
"react": "^19.2.0",
"react-dom": "^19.2.0"
}
}

View File

@@ -0,0 +1,23 @@
using Workerd = import "/workerd/workerd.capnp";
const config :Workerd.Config = (
services = [
(name = "main", worker = .mainWorker),
],
sockets = [
( name = "http",
address = "*:3001",
http = (),
service = "main"
),
]
);
const mainWorker :Workerd.Worker = (
modules = [
(name = "worker", esModule = embed "react-hello-world.workerd.js"),
],
compatibilityDate = "2025-01-01",
compatibilityFlags = ["nodejs_compat_v2"],
);

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,40 @@
// Cloudflare Workers version with export default fetch
// Run with: workerd serve react-hello-world.workerd.config.capnp
// Polyfill MessageChannel for workerd
if (typeof MessageChannel === 'undefined') {
globalThis.MessageChannel = class MessageChannel {
constructor() {
this.port1 = { onmessage: null, postMessage: () => {} };
this.port2 = {
postMessage: (msg) => {
if (this.port1.onmessage) {
queueMicrotask(() => this.port1.onmessage({ data: msg }));
}
}
};
}
};
}
import React from "react";
import { renderToReadableStream } from "react-dom/server";
const headers = {
"Content-Type": "text/html",
};
const App = () => (
<html>
<body>
<h1>Hello World</h1>
<p>This is an example.</p>
</body>
</html>
);
export default {
async fetch(request) {
return new Response(await renderToReadableStream(<App />), { headers });
},
};

View File

@@ -645,6 +645,108 @@ Valid values are:
{% /table %}
### `install.publicHoistPattern`
Controls which dependencies are hoisted to the root `node_modules` directory when using isolated installs. By default, no dependencies are hoisted when using `linker = "isolated"`.
This setting is identical to pnpm's `publicHoistPattern`. Patterns match against the dependency **name** only (not the full package path or version).
```toml
[install]
linker = "isolated"
# Hoist all @types packages to root node_modules
publicHoistPattern = "@types/*"
```
You can specify multiple patterns as an array:
```toml
[install]
linker = "isolated"
# Hoist @types packages and packages matching *eslint*
publicHoistPattern = ["@types/*", "*eslint*"]
```
Use exclusion patterns (prefixed with `!`) to prevent specific packages from being hoisted:
```toml
[install]
linker = "isolated"
# Hoist everything except no-deps
publicHoistPattern = ["*", "!no-deps"]
```
Common use cases:
```toml
[install]
linker = "isolated"
# Hoist all type definitions to avoid TypeScript issues
publicHoistPattern = ["@types/*"]
# Hoist specific build tools that scan node_modules
publicHoistPattern = ["eslint*", "prettier*", "@types/*"]
# Prevent all hoisting (most strict isolation)
publicHoistPattern = ["!*"]
```
This setting can also be configured via `.npmrc`:
```ini
# Single pattern
public-hoist-pattern=@types/*
# Multiple patterns (array syntax)
public-hoist-pattern[]=@types/*
public-hoist-pattern[]=*eslint*
```
**Note:** Direct dependencies are always installed in the project's `node_modules` regardless of this setting. This pattern only affects transitive dependencies.
### `install.hoistPattern`
Controls which dependencies are hoisted within each package's isolated `node_modules` directory when using isolated installs. Default `["*"]` (hoist all dependencies within each package).
This setting is identical to pnpm's `hoistPattern`. Patterns match against the dependency **name** only (not the full package path or version).
```toml
[install]
linker = "isolated"
# Only hoist @types packages within each package's node_modules
hoistPattern = ["@types/*"]
```
Unlike `publicHoistPattern` which controls hoisting to the root, `hoistPattern` controls hoisting within each package's own isolated `node_modules` directory.
```toml
[install]
linker = "isolated"
# Most strict: don't hoist anything within packages
hoistPattern = ["!*"]
# Default: hoist everything within packages
hoistPattern = ["*"]
# Custom: only hoist specific patterns
hoistPattern = ["@babel/*", "@types/*"]
```
This setting can also be configured via `.npmrc`:
```ini
# Single pattern
hoist-pattern=@types/*
# Multiple patterns (array syntax)
hoist-pattern[]=@babel/*
hoist-pattern[]=@types/*
```
**Note:** For most use cases, the default `hoistPattern = ["*"]` works well. You typically only need to configure `publicHoistPattern` to control what gets hoisted to the workspace root.
### `install.minimumReleaseAge`
Configure a minimum age (in seconds) for npm package versions. Package versions published more recently than this threshold will be filtered out during installation. Default is `null` (disabled).

View File

@@ -901,9 +901,6 @@ const NodeHTTPServerSocket = class Socket extends Duplex {
req.destroy();
}
}
// Emit close event on the socket to trigger onServerResponseClose
callCloseCallback(this);
}
#onCloseForDestroy(closeCallback) {
this.#onClose();

View File

@@ -392,7 +392,9 @@ EventEmitterPrototype.removeAllListeners = function removeAllListeners(type) {
// emit in LIFO order
const listeners = events[type];
for (let i = listeners.length - 1; i >= 0; i--) this.removeListener(type, listeners[i]);
if (listeners !== undefined) {
for (let i = listeners.length - 1; i >= 0; i--) this.removeListener(type, listeners[i]);
}
return this;
};

View File

@@ -108,7 +108,13 @@ pub const List = struct {
const a = ctx.generated[a_index];
const b = ctx.generated[b_index];
return a.lines.zeroBased() < b.lines.zeroBased() or (a.lines.zeroBased() == b.lines.zeroBased() and a.columns.zeroBased() <= b.columns.zeroBased());
if (a.lines.zeroBased() != b.lines.zeroBased()) {
return a.lines.zeroBased() < b.lines.zeroBased();
}
if (a.columns.zeroBased() != b.columns.zeroBased()) {
return a.columns.zeroBased() < b.columns.zeroBased();
}
return a_index < b_index;
}
};
@@ -449,7 +455,7 @@ pub fn parse(
}
source_index += source_index_delta.value;
if (source_index < 0 or source_index > sources_count) {
if (source_index < 0 or source_index >= sources_count) {
return .{
.fail = .{
.msg = "Invalid source index value",

View File

@@ -1,58 +0,0 @@
// https://github.com/oven-sh/bun/issues/14697
// node:http ServerResponse should emit close event when client disconnects
import { expect, test } from "bun:test";
import { createServer } from "node:http";
test("ServerResponse emits close event when client disconnects", async () => {
const events: string[] = [];
let resolveTest: () => void;
const testPromise = new Promise<void>(resolve => {
resolveTest = resolve;
});
const server = createServer((req, res) => {
req.once("close", () => {
events.push("request-close");
});
res.once("close", () => {
events.push("response-close");
// Both events should have fired by now
resolveTest();
});
// Don't send any response, let the client disconnect
});
await new Promise<void>(resolve => {
server.listen(0, () => resolve());
});
const port = (server.address() as any).port;
// Connect and immediately disconnect
const socket = await Bun.connect({
hostname: "localhost",
port,
socket: {
open(socket) {
// Send a minimal HTTP request
socket.write("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
// Immediately close the connection
socket.end();
},
data() {},
error() {},
close() {},
},
});
// Wait for both close events to fire
await testPromise;
server.close();
// Both request and response should have emitted close events
expect(events).toContain("request-close");
expect(events).toContain("response-close");
});

View File

@@ -0,0 +1,81 @@
// https://github.com/oven-sh/bun/issues/24147
// EventEmitter: this._events becomes undefined when removeAllListeners()
// called from event handler with removeListener meta-listener
import { EventEmitter } from "events";
import assert from "node:assert";
import { test } from "node:test";
test("removeAllListeners() from event handler with removeListener meta-listener", () => {
const emitter = new EventEmitter();
emitter.on("test", () => {
// This should not crash even though there are no 'foo' listeners
emitter.removeAllListeners("foo");
});
// Register a removeListener meta-listener to trigger the bug
emitter.on("removeListener", () => {});
// This should not throw
assert.doesNotThrow(() => emitter.emit("test"));
});
test("removeAllListeners() with actual listeners to remove", () => {
const emitter = new EventEmitter();
let fooCallCount = 0;
let removeListenerCallCount = 0;
emitter.on("foo", () => fooCallCount++);
emitter.on("foo", () => fooCallCount++);
emitter.on("test", () => {
// Remove all 'foo' listeners while inside an event handler
emitter.removeAllListeners("foo");
});
// Track removeListener calls
emitter.on("removeListener", () => {
removeListenerCallCount++;
});
// Emit test event which triggers removeAllListeners
emitter.emit("test");
// Verify listeners were removed
assert.strictEqual(emitter.listenerCount("foo"), 0);
// Verify removeListener was called twice (once for each foo listener)
assert.strictEqual(removeListenerCallCount, 2);
// Verify foo listeners were never called
assert.strictEqual(fooCallCount, 0);
});
test("nested removeAllListeners() calls", () => {
const emitter = new EventEmitter();
const events: string[] = [];
emitter.on("outer", () => {
events.push("outer-start");
emitter.removeAllListeners("inner");
events.push("outer-end");
});
emitter.on("inner", () => {
events.push("inner");
});
emitter.on("removeListener", type => {
events.push(`removeListener:${String(type)}`);
});
// This should not crash
assert.doesNotThrow(() => emitter.emit("outer"));
// Verify correct execution order
assert.deepStrictEqual(events, ["outer-start", "removeListener:inner", "outer-end"]);
// Verify inner listeners were removed
assert.strictEqual(emitter.listenerCount("inner"), 0);
});