Skip to content

node-fallbacks: fix the util polyfill and lint the polyfills for undeclared names - #40072

Closed
robobun wants to merge 5 commits into
mainfrom
farm/1286800a/util-polyfill-undeclared
Closed

robobun wants to merge 5 commits into
mainfrom
farm/1286800a/util-polyfill-undeclared

Conversation

@robobun

@robobun robobun commented Aug 22, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • util.promisify() from a bun build --target=browser bundle throws ReferenceError: kCustomPromisifiedSymbol is not defined (src/node-fallbacks/util.js:582). The polyfill is a hand conversion of the npm util package and the declaration was dropped.
  • The same conversion broke more. callbackify() and deprecate() spread their arguments into Function#apply, so the callback gets no result and deprecate(fn)(1, 2) throws. callbackify() uses process.nextTick and isBuffer() uses instanceof Buffer. Browsers have neither. The default export lists 8 of 28 exports, so util.format is undefined.
  • Nothing checked the polyfill sources for undeclared names. node-fallbacks: build the once() promise without $newPromiseCapability #40049 fixed the same class in events.js. Its self-review found this file.

Fix

  • util.js: declare the symbol, pass the arrays to apply, use queueMicrotask, duck-type the buffer polyfill in isBuffer() as upstream util does in its browser build, read globalThis.process, and complete the default export. path.resolve() falls back to / without process.cwd().
  • Lint src/node-fallbacks/*.js with no-undef under the browser environment (oxlint.json override, lint script). A node global or a JSC intrinsic in a polyfill source now fails CI. It flags the pre-node-fallbacks: build the once() promise without $newPromiseCapability #40049 events.js.
  • Correct because the polyfill runs in the user's browser and can only use what a browser has. queueMicrotask keeps the node property that matters: a throw in the callback is an uncaught error, not a rejection.
  • Verified: test/bundler/bundler_browser.test.ts (new browser/NodeUtilPolyfill, stock bun fails it). The test deletes process, Buffer and setImmediate before the bundle runs.

Background

  • src/node-fallbacks/*.js are browser polyfills for node:* modules. bun build --target=browser inlines them into the user's bundle.
  • promisify.custom is Symbol.for("nodejs.util.promisify.custom"). A function can carry its own promisified form under it.
Notes

Repro with the released binary. Bundle this with bun build --target=browser, then run the output with node after delete globalThis.process; delete globalThis.Buffer to match a browser:

import util, { promisify, callbackify, deprecate, isBuffer } from "node:util";
import { Buffer } from "node:buffer";
await promisify((a, cb) => cb(null, a * 2))(21);          // ReferenceError: kCustomPromisifiedSymbol
callbackify(async a => a + 1)(41, (err, v) => {});        // ReferenceError: process is not defined
deprecate((a, b) => a + b, "old")(1, 2);                  // TypeError: CreateListFromArrayLike called on non-object
isBuffer(Buffer.from("x"));                               // false, or ReferenceError without a Buffer global
typeof util.format;                                       // "undefined"

With the fix the same file prints 42, null,42, 3, true, function.

The test runs the bundle under bun, which has every node global. It saves process, deletes process, Buffer and setImmediate from globalThis, and exits on an unhandled rejection. With the debug build's polyfill output edited by hand, a process.nextTick revert fails with unhandledRejection: process is not defined, and a synchronous cb(null, ret) fails with unhandledRejection: in callback where the test expects uncaughtException: in callback.

The lint covers the hand-written sources only. The vendored packages that assert.js, http.js, stream.js and zlib.js bundle still reference process internally. #35484 covers the process shim for the stream polyfill.

Lint findings on main before this PR, with the new override and no process global: util.js kCustomPromisifiedSymbol (6), pid (1), Buffer (1), process (5), path.js process (1). Nothing else in the 20 polyfill sources. With the pre-#40049 events.js swapped in, the lint also reports $newPromiseCapability.

debuglog() reads globalThis.process?.env?.NODE_DEBUG at run time now. Before, build-fallbacks.ts defined process.env.NODE_DEBUG as false at build time, which also made the match regex /^false$/.

build-fallbacks.ts is linted with the base rules now. Its two no-unused-vars hits were dead locals (mod, file, externals, i, and the unused text parameter). Removed. Its output checks (string matches for a few known bad names) stay as they are.

Suites run: test/bundler/bundler_browser.test.ts (24 pass, 1 skip, 1 todo).


[decide:dep] gate passed · iteration 1 · 6 files touched

fails on main (without fix)
ASAN without fix: 1 failed, 2 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/bundler_browser.test.ts
bun test v1.4.1 (4448a2e21)

test/bundler/bundler_browser.test.ts:
(pass) bundler > browser/NodeBuffer#21522 [1620.55ms]
(pass) bundler > browser/NodeBuffer#12272 [828.03ms]
(pass) bundler > browser/NodeFS [357.81ms]
(pass) bundler > browser/NodeTTY [359.50ms]
(pass) bundler > browser/NodeEventsOnce [517.46ms]
1815 |           }
1816 |         } else if (!success) {
1817 |           if (run.exitCode) {
1818 |             expect([exitCode, signalCode]).toEqual([run.exitCode, undefined]);
1819 |           } else {
1820 |             throw new Error(prefix + "Runtime failed\n" + stdout!.toUnixString() + "\n" + stderr!.toUnixString());
                             ^
error: Runtime failed

364 |   return Object.prototype.hasOwnProperty.call(obj, prop);
365 | }
366 | var promisify = ((x) => (x.custom = Symbol.for("nodejs.util.promisify.custom"), x))(function(original) {
367 |   if (typeof original !== "function")
368 |     throw TypeError('The "original" argument must be of type Function');
369 |   if (kC
... (truncated)

release without fix: 2 skipped
bun test v1.4.1-canary.1 (aa455a21c)

test/bundler/bundler_browser.test.ts:
(pass) bundler > browser/NodeBuffer#21522 [33.53ms]
(pass) bundler > browser/NodeBuffer#12272 [16.82ms]
(pass) bundler > browser/NodeFS [10.28ms]
(pass) bundler > browser/NodeTTY [10.85ms]
(pass) bundler > browser/NodeEventsOnce [13.49ms]
(pass) bundler > browser/NodeUtilPolyfill [21.86ms]
(pass) bundler > browser/NodeUrlProtocolTablesIgnorePrototype [14.34ms]
(pass) bundler > browser/NodePolyfills [149.10ms]
(todo) bundler > browser/NodePolyfillExternal
(pass) bundler > browser/BrowserFieldDisablesPolyfilledBuiltin#4928 [12.59ms]
(pass) bundler > browser/BrowserFieldDisablesPolyfilledBuiltinNodePrefix#4928 [13.26ms]
(pass) bundler > browser/BrowserFieldRemapsPolyfilledBuiltin#4928 [9.69ms]
(pass) bundler > browser/BrowserFieldDisabledBuiltinStillPolyfillsOutsideScope [13.77ms]
(pass) bundler > browser/EntryPointDisabledByBrowserField [5.38ms]
(pass) bundler > browser/EntryPointDisabledByBrowserFieldNextToLiveEntryPoint [4.87ms]
(pass) bundler > browser/EntryPointDisabledByBrowserFieldOnlyAppliesToBrowserTarget [11.67ms]
(pass) bundler > browser/EntryPointDisabledByPackageMainBrowserField [5
... (truncated)
passes on PR (with fix)
ASAN with fix: 2 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/bundler_browser.test.ts
bun test v1.4.1 (4448a2e21)

test/bundler/bundler_browser.test.ts:
(pass) bundler > browser/NodeBuffer#21522 [1571.31ms]
(pass) bundler > browser/NodeBuffer#12272 [697.47ms]
(pass) bundler > browser/NodeFS [365.68ms]
(pass) bundler > browser/NodeTTY [349.38ms]
(pass) bundler > browser/NodeEventsOnce [524.67ms]
(pass) bundler > browser/NodeUtilPolyfill [1050.31ms]
(pass) bundler > browser/NodeUrlProtocolTablesIgnorePrototype [463.97ms]
(pass) bundler > browser/NodePolyfills [7825.42ms]
(todo) bundler > browser/NodePolyfillExternal
(pass) bundler > browser/BrowserFieldDisablesPolyfilledBuiltin#4928 [436.23ms]
(pass) bundler > browser/BrowserFieldDisablesPolyfilledBuiltinNodePrefix#4928 [719.04ms]
(pass) bundler > browser/BrowserFieldRemapsPolyfilledBuiltin#4928 [387.81ms]
(pass) bundler > browser/BrowserFieldDisabledBuiltinStillPolyfillsOutsideScope [472.16ms]
(pass) bundler > browser/EntryPointDisabledByBrowserField [227.19ms]
(pass) bundler > browser/EntryPointDisabledByBrowserFieldNextToLiveEntryPo
... (truncated)

release with fix: 2 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 648ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/6] gen node-fallbacks/*.js
[1/6] cargo bun_runtime → libbun_runtime.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m   Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[92m   Compiling�[0m bun_base64 v0.0.0 (/workspace/bun/src/base64)
�[1m�[92m   Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
�[1m�[92m   Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
�[1m�[92m   Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
�[1m�[92m   Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
�[1m�[92m   Compiling�[0m bun_brotli v0.0.0 (/workspace/bun/src/brotli
... (truncated)
diff hotspot
oxlint.json                           | 17 ++++++
 package.json                          |  2 +-
 src/node-fallbacks/build-fallbacks.ts |  9 +---
 src/node-fallbacks/path.js            |  2 +-
 src/node-fallbacks/util.js            | 97 +++++++++++++++++++++++------------
 test/bundler/bundler_browser.test.ts  | 78 ++++++++++++++++++++++++++++
 6 files changed, 162 insertions(+), 43 deletions(-)

gate history · 2 passed · 0 rejected · iteration 1

evidence per changed file
file                                   reads  edits  tests
oxlint.json                                3      4      0
package.json                               0      0      0
src/node-fallbacks/build-fallbacks.ts      2      3      0
src/node-fallbacks/path.js                 1      1      0
src/node-fallbacks/util.js                 7     11      0
test/bundler/bundler_browser.test.ts       5      5      0

@coderabbitai

coderabbitai Bot commented Aug 22, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Your included review limit has been reached.

You’re in a promotional period — use the checkbox below to run this review for free:

  • Run review for free

On-demand reviews are free for the next 29 days. After that, they cost $0.25 per reviewed file.

How can I continue?

Run this review now using the option above, or comment @coderabbitai review --use-credits.

You can also wait for the limit to reset (next review available in 2 minutes), then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7fcc6ea2-efb1-4ff7-aa15-f3238a85a12b

📥 Commits

Reviewing files that changed from the base of the PR and between abe2ad4 and e236c3a.

📒 Files selected for processing (6)
  • oxlint.json
  • package.json
  • src/node-fallbacks/build-fallbacks.ts
  • src/node-fallbacks/path.js
  • src/node-fallbacks/util.js
  • test/bundler/bundler_browser.test.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Aug 22, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 6:54 AM PT - Aug 22nd, 2026

✅ @robobun, your commit e236c3a9cd1e8e818132814070dc28965a0521f3 passed in Build #103593! 🎉


🧪   To try this PR locally:

bunx bun-pr 40072

That installs a local version of the PR into your bun-40072 executable, so you can run:

bun-40072 --bun

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — focused polyfill bugfixes with a lint guard to catch the class going forward.

What was reviewed:

  • util.js: each fix (kCustomPromisifiedSymbol declaration, .apply(this, args), queueMicrotask, isBuffer duck-typing, debuglog pid) checked against the upstream npm util browser build; all match.
  • build-fallbacks.ts: confirmed the removed locals (mod/file/externals/i/text) were dead — the live external list is externalModules, computed separately.
  • oxlint.json override: process as a declared global is needed because the polyfills guard it with typeof; stream.js is the only CJS file in the directory.
  • New test exercises success and error paths for each fixed export, and itBundled runs the browser bundle (not bun's native node:util), so the polyfill code is what executes.
Extended reasoning...

Overview

Fixes five broken exports in the node:util browser polyfill (src/node-fallbacks/util.js) that bun build --target=browser inlines into user bundles: an undeclared kCustomPromisifiedSymbol that made promisify() throw on every call, two fn.apply(this, ...args) sites that spread instead of passing the array, callbackify() calling process.nextTick (not a browser global), isBuffer() doing instanceof Buffer (same), and debuglog() referencing an undeclared pid. Adds an oxlint no-undef override under the browser env for src/node-fallbacks/*.js and wires the directory into bun run lint, so the bug class fails CI. Deletes dead locals from build-fallbacks.ts that the new lint surfaced. Adds browser/NodeUtilPolyfill in test/bundler/bundler_browser.test.ts.

Security risks

None. This is a browser-side polyfill — plain JS embedded into user bundles, never loaded by the Bun runtime. No auth, crypto, filesystem, or network. The isBuffer duck-typing loosens a type check, but that mirrors the upstream util package's browser build exactly and isBuffer is advisory, not a security gate.

Level of scrutiny

Low-to-medium. The polyfill is not on any Bun runtime hot path; it only affects --target=browser bundles. Each change is a small, mechanical correction whose before/after is obvious from the diff (undeclared → declared, spread-into-apply → array-into-apply, node-only global → web-standard equivalent). The lint addition is additive tooling and the PR verified it is clean on main after the fixes.

Other factors

  • The test covers each fixed export with both success and error paths (promisify custom symbol, callbackify falsy rejection wrapping, deprecate warn-once with this binding, isBuffer negatives). itBundled runs the bundled output in a subprocess, so console.error mutation in the fixture stays isolated.
  • Checked that hoisting kCustomPromisifiedSymbol to a top-level const matches the file's existing pattern (formatRegExp, months are already un-annotated top-level consts), so tree-shaking behavior is unchanged relative to the rest of the file.
  • queueMicrotask preserves the property the comment names: a throw inside the callback surfaces as an uncaught error, not a rejection of the settled promise — same as process.nextTick.
  • The build-fallbacks.ts deletions are verifiably dead: the live external list is built from builtins.concat(moduleFiles.filter(...)) into externalModules; the removed externals/mod/file/i block computed the same thing and discarded it.
  • No prior reviewer comments to address; no CODEOWNER on these paths.

…clared names

The browser polyfill for node:util was converted by hand from the npm
util package. Several exports did not survive the conversion:

- promisify() read kCustomPromisifiedSymbol, which was never declared,
  so every call threw ReferenceError.
- callbackify() spread the callback arguments into Function#apply, so
  the callback never received the result. It also called
  process.nextTick, and browsers have no process global.
- deprecate() spread its arguments into Function#apply the same way.
- isBuffer() used instanceof against a Buffer global that browsers do
  not have. It now duck-types the buffer polyfill like upstream does.
- debuglog() read an undeclared pid in its enabled branch.

Lint src/node-fallbacks/*.js with no-undef under the browser
environment so an undeclared name (a JSC intrinsic, a dropped
declaration) fails CI. Remove the dead locals in build-fallbacks.ts
that the lint flagged.
@robobun
robobun force-pushed the farm/1286800a/util-polyfill-undeclared branch from cf6e979 to f332214 Compare August 22, 2026 09:15
Comment thread src/node-fallbacks/util.js Outdated
Comment thread src/node-fallbacks/util.js Outdated
Comment thread test/bundler/bundler_browser.test.ts

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the prior review's coverage gap (no guard on the queueMicrotask change) is now closed by the onAfterBundle assertion in 594d8a2.

What was reviewed:

  • Each util.js fix traced to its bug: undeclared kCustomPromisifiedSymbol/pid/Buffer, spread-into-.apply, process.nextTick in a browser bundle. isBuffer duck-typing matches upstream browserify util.
  • Confirmed the build-fallbacks.ts deletions were dead locals — the first loop's mod/file that populate moduleFiles remain.
  • oxlint override: whitelisting process is deliberate (guarded reads elsewhere); the onAfterBundle string check compensates for the one unguarded case that would slip through.
Extended reasoning...

Overview

Fixes five conversion bugs in the node:util browser polyfill (src/node-fallbacks/util.js) that made promisify, callbackify, deprecate, isBuffer, and debuglog throw or misbehave in a browser bundle. Adds an oxlint no-undef override for src/node-fallbacks/*.js under the browser env so undeclared names fail CI, wires src/node-fallbacks into the lint script, deletes dead locals from build-fallbacks.ts that the new lint scope surfaced, and adds a browser/NodeUtilPolyfill bundler test exercising every fixed export.

Security risks

None. This is plain JS shipped into the user's browser bundle; it touches no auth, crypto, filesystem, or network code. The fixes are strictly correctness (declaring a symbol, passing an array to Function#apply, swapping process.nextTick for queueMicrotask, duck-typing Buffer).

Level of scrutiny

Low-to-medium. The polyfills are not part of the Bun runtime — they are inlined only into --target=browser bundles — so blast radius is limited to browser output that already threw ReferenceError on these paths. Every change is a small, mechanical correction with an obvious before/after (.apply(this, ...args) → .apply(this, args)), and each is covered by a specific assertion in the new test plus (for undeclared names) the new lint rule.

Other factors

All prior feedback on the PR is resolved: the two comment-cop warnings about long comments were shortened in 8a81084, and my earlier inline nit about the missing process.nextTick regression guard was addressed in 594d8a2 with an onAfterBundle check mirroring the neighboring NodeEventsOnce test. Hoisting kCustomPromisifiedSymbol to a module-level const moves one Symbol.for call outside the /* @__PURE__ */ IIFE, but that is a negligible one-time evaluation and necessary for the function body to reference it. The build-fallbacks.ts cleanup is safe — the removed block recomputed values already produced by the earlier loop and never read them. No design decisions or API surface changes here; approving.

…ss, guard path.resolve

import util from "node:util" gave an object with 8 of the 28 exports, so
util.format and util.inspect were undefined in browser bundles. The
default export now lists every named export.

deprecate() and debuglog() read globalThis.process instead of the bare
process global, and path.resolve() falls back to "/" when there is no
process.cwd(). With no bare process left in the hand-written polyfills,
the lint no longer declares process as a global, so a new unguarded use
is a no-undef error.

The browser bundle test now deletes process, Buffer and setImmediate
before the polyfills run, exits on an unhandled rejection, and checks
that a throw inside a callbackify callback is an uncaught exception.
This replaces the string check for process.nextTick.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the follow-up commit (aa455a2) resolved my prior note in a stronger way than I suggested: dropping the process lint whitelist and reading globalThis.process everywhere means a bare process.nextTick now fails no-undef, and the test's delete globalThis.process catches a globalThis.process.nextTick regression at runtime.

What was reviewed:

  • util.js: kCustomPromisifiedSymbol declared, .apply(this, args) (not spread), queueMicrotask for callbackify, isBuffer duck-typing matches upstream browserify util, debuglog no longer builds a bogus regex when NODE_DEBUG is unset, default export now covers every named export.
  • build-fallbacks.ts: only dead locals removed; externalModules (the live one) is untouched and globalThis.process is not caught by the global.process string replace.
  • Confirmed no bare process remains in any src/node-fallbacks/*.js, so the new no-undef override is clean without a globals whitelist.
Extended reasoning...

Overview

Fixes the node:util browser polyfill (src/node-fallbacks/util.js) — a hand-conversion of the npm util package that had dropped the kCustomPromisifiedSymbol declaration, spread arrays into Function#apply, referenced bare process/Buffer/pid, and shipped a default export missing 20 of its 28 members. Also guards path.resolve() against a missing process.cwd(), adds an oxlint override that runs no-undef over src/node-fallbacks/*.js under the browser environment, wires src/node-fallbacks into bun run lint, deletes dead locals from build-fallbacks.ts, and adds a comprehensive browser/NodeUtilPolyfill bundler test.

Security risks

None. These files are plain-JS polyfills inlined into user bundles by bun build --target=browser; the runtime never loads them. No auth, crypto, or filesystem paths touched. The isBuffer duck-typing matches upstream browserify util and is not used for any trust decision.

Level of scrutiny

Medium-low. The blast radius is limited to browser-target bundler output — a path that was already broken (promisify threw a ReferenceError on every call). Each fix is mechanical and traces directly to the upstream npm util package the file was ported from. The lint addition is additive (a new override) and I grepped every *.js in src/node-fallbacks/ to confirm no bare process remains, so CI won't go red on unrelated files.

Other factors

All prior feedback is resolved. The comment-cop notes were fixed in 8a81084. My previous review asked for a regression guard on the queueMicrotask clause; the author first added the onAfterBundle string check I suggested (594d8a2), then in aa455a2 replaced it with a superior structural fix — removing the process global whitelist so lint flags any bare process, and having the test delete globalThis.process/Buffer/setImmediate before exercising the polyfill. That covers both the bare-process and globalThis.process regression shapes. The new test asserts exact stdout for promisify (both paths + promisify.custom), callbackify (resolve/reject/falsy-reject/uncaught-in-callback), deprecate (with and without process), isBuffer, debuglog, path.resolve, and default-export completeness. The build-fallbacks.ts diff is pure dead-code removal; the live externalModules computation is unchanged.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the earlier feedback about guarding the queueMicrotask clause is addressed: the test now deletes process/Buffer/setImmediate before exercising the polyfills and exits on unhandled rejection, so a process.nextTick revert fails at runtime.

What was reviewed:

  • util.js: declared kCustomPromisifiedSymbol, fixed .apply(this, ...args) → .apply(this, args) in deprecate/callbackify, queueMicrotask in callbackify, duck-typed isBuffer, debuglog no longer references pid or builds a /^false$/ regex, and the default export now mirrors every named export.
  • path.js resolve() falls back to / when globalThis.process is absent; grepped the other polyfill sources and no bare process/Buffer references remain, so the new no-undef browser-env override passes without a globals allowlist.
  • build-fallbacks.ts deletions are provably-dead locals; the --define=process.env.NODE_DEBUG flag still applies to vendored deps and is left in place.
  • browser/NodePolyfills snapshot is unaffected (only the default export changed, not named exports).
Extended reasoning...

Overview

Fixes multiple bugs in the node:util browser polyfill (src/node-fallbacks/util.js) introduced when it was hand-converted from the npm util package to an ES module: the undeclared kCustomPromisifiedSymbol, fn.apply(this, ...args) spreads that should have been fn.apply(this, args), unguarded process.nextTick/process/Buffer/pid references, and an incomplete default export. Also guards path.resolve() for the no-process.cwd case, adds an oxlint no-undef override for src/node-fallbacks/*.js under the browser environment, extends the lint script to cover that directory, removes dead locals from build-fallbacks.ts, and adds browser/NodeUtilPolyfill in test/bundler/bundler_browser.test.ts.

Security risks

None. These are browser polyfills bundled into user output for --target=browser; they do not touch Bun's runtime node:util, native code, auth, crypto, or filesystem paths. The oxlint override is additive and scoped to one directory.

Level of scrutiny

Low-to-medium. The polyfills are plain JS derived from a well-known npm package, and every change is either a mechanical correctness fix (declare a const, pass an array not a spread, use a browser-available scheduler) or a defensive guard (globalThis.process?. instead of bare process). The blast radius is limited to bun build --target=browser output that imports node:util/node:path, which was already broken (promisify threw a ReferenceError on every call). No design decisions here — the reference is the upstream util package's browser build, which the PR follows for isBuffer duck-typing and callbackify semantics.

Other factors

  • Prior feedback addressed. The comment-cop bot's two long-comment flags were resolved (comments shortened in 8a81084). My earlier inline comment about the queueMicrotask clause lacking a regression guard was resolved twice: first with an onAfterBundle string check (594d8a2), then replaced with a stronger runtime guard (aa455a2) — the test deletes process, Buffer, and setImmediate before calling into the polyfills and installs an unhandledRejection handler that exits nonzero, so a bare process.nextTick in callbackify now ReferenceErrors instead of silently passing under bun. The globals: { process } allowlist that motivated the original comment was also dropped from the oxlint override in the same commit.
  • Test coverage. The new test exercises every fixed export (promisify success/error/custom, callbackify success/rejection/falsy-rejection/uncaught-throw, deprecate with and without process, isBuffer positive/negative, debuglog, path.resolve fallback, default-vs-namespace parity). PR evidence shows it fails on main and passes on the PR in both ASAN-debug and release. The existing browser/NodePolyfills snapshot still passes because only the default export grew, not the named-export set.
  • Lint scope. I grepped src/node-fallbacks/*.js for bare process — only comments and process.js (the shim itself) remain, so the new no-undef override passes without whitelisting node globals. stream.js gets a separate commonjs env override because it is built as CJS.
  • Dead-code removal. The four locals removed from build-fallbacks.ts (mod, file, externals, i) and the unused text parameter were computed and never read; removing them is required for the file to lint clean now that src/node-fallbacks is on the lint path.

@robobun

robobun commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favor of #41533. It contains the util.js and path.js changes of this PR byte for byte, the oxlint no-undef override for src/node-fallbacks/*.js, and browser/NodeUtilPolyfill unchanged, together with the other browser polyfill fixes.

@robobun robobun closed this Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant