Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f4d755a
usockets: tell a socket's holder it is gone exactly once, whoever clo…
dylan-conway Oct 1, 2026
faac63e
fetch: decide zlib or raw deflate by the RFC 1950 header, for both de…
robobun Oct 2, 2026
468efac
Remove the dead --dump-environment-variables flag from bun build (#44…
robobun Oct 2, 2026
49c076e
structured clone: re-check the transfer list after serializing so a f…
robobun Oct 2, 2026
bc7a813
glob: validate continuation bytes when stepping over directory entry …
robobun Oct 2, 2026
76cf78c
Fix a segfault when Module.runMain is not a function, and report what…
dylan-conway Oct 2, 2026
fa467dc
bun test: fix a silent stack overflow when printing a deeply nested v…
dylan-conway Oct 2, 2026
e196cd6
bun test: fix a segfault when an asymmetric matcher meets an array el…
dylan-conway Oct 2, 2026
e29a7ca
Blob: return only the slice when consuming a slice's stream after the…
robobun Oct 2, 2026
369615c
node:tls: check the server's name for a handshake that completes afte…
robobun Oct 2, 2026
ffe2f15
node:tls: make client-side new TLSSocket(socket) upgrade the socket i…
robobun Oct 2, 2026
c5a68b0
timers: store [util.promisify.custom] as a per-function accessor (#44…
robobun Oct 2, 2026
a11362e
tls: drop the handshake records that are sealed after our own FIN (#4…
robobun Oct 2, 2026
519963e
node:http: a thrown dispatch reads the role of its response from the …
robobun Oct 3, 2026
80de08a
install: deliver a manifest task's waiters on every pass of run_tasks…
robobun Oct 3, 2026
272ff43
Resolve a module specifier once: fix a segfault in require() of an ES…
dylan-conway Oct 3, 2026
7883b30
Remove four lifetime erasures that nothing needs (#44494)
dylan-conway Oct 3, 2026
ebef46f
Bump WebKit to 1600131e46b5 (#44500)
dylan-conway Oct 3, 2026
f4281d1
Do not auto-install a bare name that is a runtime plugin's own (#44492)
dylan-conway Oct 3, 2026
403209c
HTMLRewriter: hold onEndTag callbacks in a visited slot, and stop usi…
robobun Oct 3, 2026
355a86e
tls: end the wrapped Duplex on end(), also before the handshake compl…
robobun Oct 3, 2026
8f6a13a
bundler: fix a segfault in `bun build --sourcemap` when the link step…
dylan-conway Oct 3, 2026
83b2d4f
bun test: --coverage-reporter implies --coverage (#44469)
robobun Oct 3, 2026
7a503a7
bundler: print a file where it is imported, not where its bindings ar…
dylan-conway Oct 3, 2026
dea9888
chore: sync upstream main through 7a503a7899
steipete Oct 3, 2026
2d63376
fix(plugin): preserve decoded runtime importer paths
steipete Oct 3, 2026
e6dc9ee
fix(plugin): preserve authored file URL requests
steipete Oct 3, 2026
2fa474a
fix(plugin): retain redirected file URL suffixes
steipete Oct 3, 2026
64c671e
fix(resolve): preserve invalid file URL diagnostics
steipete Oct 3, 2026
f376098
chore: integrate fork main and preserve Node import-array semantics
steipete Oct 3, 2026
80f5bbd
chore: integrate latest fork fixes before upstream sync
steipete Oct 3, 2026
2ae187e
chore: preserve delegated plugin refresh through upstream sync
steipete Oct 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,9 @@

- Preserve Node's deferred JSON diagnostics for both package maps and accept JSON-encoded maps in string fields.
- Refresh missing runtime files after `Bun.plugin` hook registration so delegated resolution sees newly created package-import targets.

- Sync upstream through `7a503a7899dcf12186187c38df9f3c96b3ab9ad4`, preserving fork module hooks, plugin import kinds, URL identity, and compatibility patches while adopting resolution-once loading and WebKit `1600131e46b5af48bbda3559af8d8a3327230b6e`.

- Allow package imports to target recognized `bun:` built-ins while retaining Node 24.21 validation for unknown names, other URL schemes, and exports targets.

- Limit the `bun:` package-import exception to scalar targets outside fallback arrays, preserving Node 24.21 array selection and errors for native and captured module loading.
2 changes: 1 addition & 1 deletion completions/bun.bash
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ _bun_completions() {

local SUBCOMMANDS="dev bun create run install add remove upgrade completions discord help init pm x test repl update audit dedupe prune outdated link unlink build";

GLOBAL_OPTIONS[LONG_OPTIONS]="--use --cwd --bunfile --server-bunfile --config --disable-react-fast-refresh --disable-hmr --env-file --extension-order --jsx-factory --jsx-fragment --extension-order --jsx-factory --jsx-fragment --jsx-import-source --jsx-production --jsx-runtime --main-fields --no-summary --version --platform --public-dir --tsconfig-override --define --external --help --inject --loader --origin --port --dump-environment-variables --dump-limits --disable-bun-js";
GLOBAL_OPTIONS[LONG_OPTIONS]="--use --cwd --bunfile --server-bunfile --config --disable-react-fast-refresh --disable-hmr --env-file --extension-order --jsx-factory --jsx-fragment --extension-order --jsx-factory --jsx-fragment --jsx-import-source --jsx-production --jsx-runtime --main-fields --no-summary --version --platform --public-dir --tsconfig-override --define --external --help --inject --loader --origin --port";
GLOBAL_OPTIONS[SHORT_OPTIONS]="-c -v -d -e -h -i -l -u -p";

PACKAGE_OPTIONS[ADD_OPTIONS_LONG]="--development --optional --peer --catalog --filter";
Expand Down
4 changes: 1 addition & 3 deletions completions/bun.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -514,9 +514,7 @@ _bun_run_completion() {
'-i[Automatically install dependencies and use global cache in bun'"'"'s runtime, equivalent to --install=fallback'] \
'--prefer-offline[Skip staleness checks for packages in bun'"'"'s JavaScript runtime and resolve from disk]' \
'--prefer-latest[Use the latest matching versions of packages in bun'"'"'s JavaScript runtime, always checking npm]' \
'--silent[Don'"'"'t repeat the command for bun run]' \
'--dump-environment-variables[Dump environment variables from .env and process as JSON and quit. Useful for debugging]' \
'--dump-limits[Dump system limits. Userful for debugging]' &&
'--silent[Don'"'"'t repeat the command for bun run]' &&
ret=0

case $state in
Expand Down
2 changes: 1 addition & 1 deletion docs/bundler/plugins.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ await Bun.build({

`onLoad` and `onResolve` accept an optional `namespace` string.

Every module has a namespace. Namespaces prefix the import in transpiled code; for example, a loader with a `filter: /\.yaml$/` and `namespace: "yaml:"` transforms an import from `./myfile.yaml` into `yaml:./myfile.yaml`.
Every module has a namespace, which prefixes its path. For instance, when `onResolve` returns `{ path: "./myfile.yaml", namespace: "yaml" }`, the module is `yaml:./myfile.yaml`.

The default namespace is `"file"` and you don't need to specify it: `import myModule from "./my-module.ts"` is the same as `import myModule from "file:./my-module.ts"`.

Expand Down
2 changes: 2 additions & 0 deletions docs/runtime/nodejs-compat.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ Filesystem-entry cache snapshots are synchronized across worker threads during s

Runtime package resolution follows Node 24.21's `package.json` validation. Failures in the metadata reader, non-string `name` or `type` fields, and invalid exports condition objects throw `ERR_INVALID_PACKAGE_CONFIG` with the package path and Node's resolution context. Unselected metadata and explicit `.mjs`/`.cjs` formats remain deferred, and values Node ignores are not rejected.

Scalar string targets in package `imports` may name a recognized Bun built-in such as `bun:test` or `bun:sqlite`. Inside fallback arrays, including nested arrays and conditional targets within an array, `bun:` URLs remain invalid package targets under Node 24.21 rules, so the next alternative is tried. For example, `["bun:sqlite", "./fallback.cjs"]` selects `./fallback.cjs`. A valid relative target that names a missing file still produces a missing-module error; arrays do not retry after filesystem resolution fails. The Bun-specific scalar exception does not allow unknown `bun:` names, other URL schemes (including `node:`), or built-in targets in `exports`; those keep Node's `ERR_INVALID_PACKAGE_TARGET` validation.

Reading a selected dependency package materializes both `exports` and `imports`. Invalid JSON in either map throws `SyntaxError` with Node's JSON diagnostic. CommonJS self-reference lookup reads only `exports`; CommonJS `#imports` reads `imports` before entering ESM scope resolution. Format-only scope lookups defer map errors. String fields beginning with `{` or `[` are parsed as JSON maps, matching Node's package reader.

Unreadable selected package metadata also throws `ERR_INVALID_PACKAGE_CONFIG`, rather than falling through to an index file. This deliberately follows Node 24.21; Node 24.19 treated these read failures as absent metadata. Missing files, non-directory path components, and a `package.json` directory remain absence cases.
Expand Down
6 changes: 5 additions & 1 deletion docs/runtime/plugins.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ await Bun.build({

### Namespaces

`onLoad` and `onResolve` accept an optional `namespace` string. Every module has a namespace, which prefixes the import in transpiled code. For instance, a loader with a `filter: /\.yaml$/` and `namespace: "yaml:"` transforms an import from `./myfile.yaml` into `yaml:./myfile.yaml`.
`onLoad` and `onResolve` accept an optional `namespace` string. Every module has a namespace, which prefixes its path. For instance, when `onResolve` returns `{ path: "./myfile.yaml", namespace: "yaml" }`, the module is `yaml:./myfile.yaml`.

The default namespace is `"file"` and you don't need to specify it: `import myModule from "./my-module.ts"` is the same as `import myModule from "file:./my-module.ts"`.

Expand Down Expand Up @@ -177,6 +177,10 @@ The second argument to `onResolve()` is a callback that runs for each module imp

The callback receives the _path_ to the matching module and can return a _new path_ for it. Bun reads the contents of the _new path_ and parses it as a module.

At runtime, the callback runs once for each `import` or `require()` as it executes. A _new path_ without a `namespace` is resolved from the importing module like any other import, without running `onResolve()` callbacks on it: it can be relative, leave out its extension, or name a package. If nothing is found there, the _new path_ is used as it is when an [`onLoad()`](#onload) callback matches it, so it does not have to exist on disk.

A bare name such as `"my-virtual.js"` could also be a package in the registry, so `onResolve()` callbacks run on it once more. If one of them returns a path, the name is the plugin's own and is never [auto-installed](/runtime/auto-install).

For example, redirecting all imports to `images/` to `./public/images/`:

```ts index.ts icon="/icons/typescript.svg"
Expand Down
3 changes: 2 additions & 1 deletion docs/snippets/cli/test.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ bun test <patterns>
</ParamField>

<ParamField path="--coverage-reporter" type="string" default="text">
Report coverage in <code>text</code> and/or <code>lcov</code>. Defaults to <code>text</code>
Report coverage in <code>text</code> and/or <code>lcov</code>. Defaults to <code>text</code>. Implies{" "}
<code>--coverage</code>
</ParamField>

<ParamField path="--coverage-dir" type="string" default="coverage">
Expand Down
9 changes: 9 additions & 0 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6389,6 +6389,15 @@ declare module "bun" {
interface OnResolveResult {
/**
* The destination of the import
*
* In a runtime plugin, a path without a `namespace` is resolved from the
* importing module like any other import, without running `onResolve`
* callbacks on it. If nothing is found there, it is used as it is when an
* `onLoad` callback matches it.
*
* A bare name could also be a package in the registry, so `onResolve`
* callbacks run on it once more. If one of them returns a path, the name is
* the plugin's own and is never auto-installed.
*/
path: string;
/**
Expand Down
42 changes: 13 additions & 29 deletions packages/bun-usockets/src/context.c
Original file line number Diff line number Diff line change
Expand Up @@ -95,39 +95,24 @@ void us_socket_group_close_all_ex(struct us_socket_group_t *group, int also_list
* us_internal_socket_group_unlink_socket advances group->iterator past any
* socket it unlinks, so parking the next pointer there lets a handler free
* it without leaving us a dangling step (same pattern as the timeout sweep
* in loop.c). */
* in loop.c). A socket a handler opens links in at the head, behind the
* walk: it is not this call's to close, so a handler that always dials
* again cannot keep the walk going. */
group->iterator = group->head_sockets;
while (group->iterator) {
struct us_socket_t *s = group->iterator;
group->iterator = s->next;
if (us_internal_poll_type(&s->p) & POLL_TYPE_SEMI_SOCKET) {
/* In-flight connect — close_raw skips dispatch for SEMI_SOCKET
* (on_close without on_open is wrong), so the Zig wrapper's
* `socket = .connected` would never detach and finalize() UAFs
* after drainClosedSockets(). Deliver the same on_connect_error
* the natural failure path would have, which detaches the
* wrapper. The handler then closes; if it doesn't, the
* force-drain below catches it. */
us_dispatch_connect_error(s, ECONNABORTED);
if (!us_socket_is_closed(s)) {
us_internal_socket_close_raw(s, LIBUS_SOCKET_CLOSE_CODE_CONNECTION_RESET, 0);
}
} else {
us_socket_close(s, LIBUS_SOCKET_CLOSE_CODE_CLEAN_SHUTDOWN, 0);
us_socket_close(s, LIBUS_SOCKET_CLOSE_CODE_CLEAN_SHUTDOWN, 0);
/* A TLS socket may have *deferred* that: us_internal_ssl_close with
* code==0 sends close_notify and, on WANT_READ, waits for the peer's
* reply. Callers (e.g. Listener.deinit) free the embedding storage
* next, which would leave s->group dangling. */
if (!us_socket_is_closed(s)) {
us_internal_socket_close_raw(s, LIBUS_SOCKET_CLOSE_CODE_CONNECTION_RESET, 0);
}
}
group->iterator = 0;

/* TLS sockets may have *deferred* the close above: us_internal_ssl_close
* with code==0 sends close_notify and, on WANT_READ, leaves the socket
* open in head_sockets waiting for the peer's reply. Callers of close_all
* (e.g. Listener.deinit) free the embedding storage immediately after, so
* any survivor's s->group becomes a dangling pointer. The graceful walk
* already flushed close_notify; force-drain the rest synchronously now. */
while (group->head_sockets) {
us_internal_socket_close_raw(group->head_sockets, LIBUS_SOCKET_CLOSE_CODE_CONNECTION_RESET, 0);
}

/* Sockets parked in the loop-wide low-prio queue aren't in head_sockets
* (the queue reuses prev/next), so they'd survive the walk above and later
* dereference s->group into freed owner storage. Drain ours out now. */
Expand Down Expand Up @@ -760,7 +745,7 @@ void us_internal_socket_after_resolve(struct us_connecting_socket_t *c) {
if (result->error) {
/* Preserve the getaddrinfo failure so the connect-error callback can
* report the resolver error (ENOTFOUND, ...) instead of the fabricated
* ECONNABORTED that us_connecting_socket_close fills in when `error`
* ECANCELED that us_connecting_socket_close fills in when `error`
* is still 0. `error_is_dns` tags the namespace: getaddrinfo return
* codes and errnos overlap numerically. */
c->error = result->error;
Expand Down Expand Up @@ -820,15 +805,14 @@ void us_internal_socket_after_open(struct us_socket_t *s, int error) {
if (opened == 0 && c->connecting_head == NULL) {
/* Every resolved address failed to connect. Without this,
* us_connecting_socket_close defaults c->error to
* ECONNABORTED (caller abort) and never invalidates the
* ECANCELED (caller abort) and never invalidates the
* DNS cache entry for the dead host. */
c->error = ECONNREFUSED;
us_connecting_socket_close(c);
}
}
} else {
us_dispatch_connect_error(s, error);
// It's expected that close is called by the caller
us_internal_socket_connect_failed(s, error);
}
} else {
us_poll_change(&s->p, s->group->loop, LIBUS_SOCKET_READABLE);
Expand Down
13 changes: 12 additions & 1 deletion packages/bun-usockets/src/crypto/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,11 @@ static int BIO_s_custom_write(BIO *bio, const char *data, int length) {

BIO_clear_retry_flags(bio);
if (!written) {
if (!us_internal_socket_can_raw_write(loop_ssl_data->ssl_socket)) {
/* Sealed after our FIN, so it can never leave. A retry would wait for a
* writable event that never comes. */
return length;
}
BIO_set_retry_write(bio);
return -1;
}
Expand All @@ -614,7 +619,8 @@ static int BIO_s_custom_write(BIO *bio, const char *data, int length) {
* spills the remainder into the loop's single spill slot - SSL already
* counts those records as delivered, so they are drained (in order, to this
* socket only) from its writable event. Returns 1 when the wire took
* everything, 0 when a spill is now pending. */
* everything, 0 when it did not: the rest is spilled, or dropped when it can
* never be sent. */
static int ssl_flush_write_batch(struct loop_ssl_data *loop_ssl_data, struct us_socket_t *s) {
unsigned int len = loop_ssl_data->ssl_write_batch_len;
if (!len) return 1;
Expand All @@ -630,6 +636,11 @@ static int ssl_flush_write_batch(struct loop_ssl_data *loop_ssl_data, struct us_
if (written < 0) written = 0;
if ((unsigned int)written < len) {
unsigned int remainder = len - (unsigned int)written;
if (!us_internal_socket_can_raw_write(s)) {
/* Sealed after our FIN, so it can never leave. A spill would hold the
* loop's one spill slot, and us_internal_ssl_close would wait for it. */
return 0;
}
if (loop_ssl_data->ssl_spill_owner) {
/* The spill slot is already another socket's (a re-entrant JS region
* produced one between the entry-time gate and this flush).
Expand Down
10 changes: 10 additions & 0 deletions packages/bun-usockets/src/internal/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,14 @@ extern void __attribute__((__noreturn__)) Bun__outOfMemory(void);
#define IS_EINTR(rc) (rc == SOCKET_ERROR && WSAGetLastError() == WSAEINTR)
#define LIBUS_ERR WSAGetLastError()
#define LIBUS_ECONNRESET WSAECONNRESET
/* What libuv translates to UV_ECANCELED (uv_translate_sys_error). */
#define LIBUS_ECANCELED WSAEINTR
#else
#include <errno.h>
#define IS_EINTR(rc) (rc == -1 && errno == EINTR)
#define LIBUS_ERR errno
#define LIBUS_ECONNRESET ECONNRESET
#define LIBUS_ECANCELED ECANCELED
#endif
#include <stdbool.h>
/* Poll type and what it polls for */
Expand Down Expand Up @@ -194,6 +197,8 @@ void us_internal_group_maybe_unlink(struct us_socket_group_t *group);
* close_notify, may defer) when s->ssl. These are the underlying halves: the
* SSL path calls _raw once it's actually time to drop the fd. */
struct us_socket_t *us_internal_socket_close_raw(us_socket_r s, int code, void *reason);
/* The connect `s` was made for failed with `error`. */
void us_internal_socket_connect_failed(us_socket_r s, int error);
struct us_socket_t *us_internal_ssl_close(us_socket_r s, int code, void *reason);
int us_internal_loop_data_init(struct us_loop_t *loop,
void (*wakeup_cb)(us_loop_r loop),
Expand Down Expand Up @@ -378,6 +383,11 @@ struct us_socket_t {
_Static_assert(sizeof(struct us_socket_flags) == 1, "us_socket_flags grew");
#endif

/* Whether a raw write can send: the fd is open and no FIN went out. */
static inline int us_internal_socket_can_raw_write(struct us_socket_t *s) {
return !s->flags.is_closed && us_internal_poll_type(&s->p) != POLL_TYPE_SOCKET_SHUT_DOWN;
}

/* us_socket_adopt relocates a socket whose ext grows and retires the old block
* (is_closed + adopted, prev -> replacement; freed by the outermost tick's
* us_internal_free_closed_sockets, so it is still readable mid-dispatch). A
Expand Down
14 changes: 10 additions & 4 deletions packages/bun-usockets/src/libusockets.h
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,12 @@ struct us_bun_verify_error_t {
};

/* Immutable callback table. ~20 instances total (one per kind), all static
* const / .rodata. Nullable entries are skipped by dispatch. */
* const / .rodata. Nullable entries are skipped by dispatch.
*
* Whoever holds a socket hears that it is gone exactly once, whoever closed it:
* on_close if on_open ran, on_connect_error if the connect never completed,
* on_connecting_error for a us_connecting_socket_t. It is already closed then,
* and freed after the loop iteration. */
struct us_socket_vtable_t {
struct us_socket_t *(*on_open)(us_socket_r, int is_client, char *ip, int ip_length);
struct us_socket_t *(*on_data)(us_socket_r, char *data, int length);
Expand Down Expand Up @@ -338,8 +343,10 @@ void us_socket_group_init(us_socket_group_r group, us_loop_r loop,
* to free the embedding storage. */
void us_socket_group_deinit(us_socket_group_r group) nonnull_fn_decl;

/* Close every socket in the group (fires on_close for each). Used by server
* shutdown. The group itself stays valid. */
/* Close every socket that is in the group now; each holder hears of it (see
* us_socket_vtable_t). What a handler opens into the group meanwhile stays open, so
* an owner that frees the group next has made sure none can
* (us_socket_group_deinit asserts it). The group itself stays valid. */
void us_socket_group_close_all(us_socket_group_r group) nonnull_fn_decl;
/* As above; `also_listeners=0` leaves head_listen_sockets alone (process-exit
* teardown — listen sockets are owned by a Listener/App that frees them in
Expand Down Expand Up @@ -714,7 +721,6 @@ int us_socket_remote_port(us_socket_r s) nonnull_fn_decl;
void us_socket_remote_address(us_socket_r s, char *nonnull_arg buf, int *nonnull_arg length) nonnull_fn_decl;
void us_socket_local_address(us_socket_r s, char *nonnull_arg buf, int *nonnull_arg length) nonnull_fn_decl;

struct us_socket_t *us_socket_detach(us_socket_r s) nonnull_fn_decl;
int us_socket_ipc_write_fd(us_socket_r s, const char *data, int length, int fd) nonnull_fn_decl;
void us_socket_sendfile_needs_more(us_socket_r s) nonnull_fn_decl;
void *us_listen_socket_ext(struct us_listen_socket_t *ls) nonnull_fn_decl;
Expand Down
Loading
Loading