Skip to content

fix: let browser bundlers leave undici out - #149

Merged
scottlovegrove merged 2 commits into
mainfrom
scottlovegrove/browser-bundle
Nov 14, 2026
Merged

scottlovegrove merged 2 commits into
mainfrom
scottlovegrove/browser-bundle

Conversation

@scottlovegrove

@scottlovegrove scottlovegrove commented Nov 14, 2026 •

Copy link
Copy Markdown
Collaborator

Context

@todoist/comms-sdk can't be bundled for a browser. Bundling 5.1.0 with rspack (target: 'web') fails with 25 errors like Reading from "node:assert" is not handled by plugins (Unhandled scheme), one for each node: module undici imports.

The default transport loads undici with a dynamic import('undici') (src/transport/http-dispatcher.ts) so that it only runs on Node. In a browser it never runs: getDefaultTransport() returns undefined and requests use the global fetch. But bundlers still follow a dynamic import, so they pull undici and its Node built-ins into the build.

What was changed

  • package.json#browser maps undici to false. Bundlers building for a browser replace it with an empty module; Node ignores the field, so nothing changes there.
  • src/browser-bundle.test.ts bundles src/index.ts for the browser with rolldown, reading the package's own package.json as a consumer's bundler does. It fails if any import can't be resolved for a browser (rolldown's UNRESOLVED_IMPORT), or if undici's files end up in the bundle. Rolldown was already installed for Vite and has no install script; it's now a direct dev dependency, pinned to the version the lockfile already resolved.
  • The README says the SDK runs in a browser through a bundler.

Proof

  • The test fails without the fix: with the browser field removed it fails listing 121 UNRESOLVED_IMPORT warnings, such as Could not resolve 'node:assert' in node_modules/undici/lib/dispatcher/client.js. Rolldown bundled 113 of undici's files in that run. With the field restored it passes.
  • rspack: I installed the published 5.1.0 in an empty app and bundled it with rspack 1.4 (target: 'web'). It failed with the Unhandled scheme errors. The packed tarball from this branch compiles with no aliases. The output's only mention of undici is the SDK's own list of retryable UND_ERR_* codes in fetch-with-retry.ts; none of undici's code is there.
  • Vite: vite build of a page importing the packed tarball succeeds, with undici replaced by Vite's empty browser stand-in.
  • In a browser: I loaded the rspack bundle in headless Chrome 144 and called api.workspaces.getWorkspaces() with baseUrl set to a local stand-in for the API on the same origin. It resolved to []. The server received GET /api/v1.0/workspaces/get with Authorization: Bearer test-token.
  • npm run verify passes.

Not verified:

  • webpack and Metro. They document the same browser field, but I only ran rspack, Vite, rolldown and esbuild.
  • Uploads and other endpoints in a browser: only getWorkspaces() ran there.

The SDK could not be bundled for a browser. The default transport loads
undici with a dynamic import() so it never runs outside Node, but
bundlers still follow the import and pull in undici, whose node:
built-ins fail a browser build: rspack stops with 'Reading from
"node:assert" is not handled by plugins', esbuild with 'Could not
resolve "node:assert"'.

package.json#browser now maps undici to false, which bundlers read as
an empty module in a browser build. In a browser the SDK already uses
the global fetch, so nothing calls it.

A test bundles src/index.ts for the browser with esbuild, added as a
dev dependency, and fails on the build's errors or if undici's files
reach the bundle.
@scottlovegrove scottlovegrove self-assigned this Nov 14, 2026

@doistbot doistbot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The PR lets browser bundlers leave undici out by mapping it to an empty module in package.json#browser, with a new esbuild-based regression test proving undici's files stay out of a browser bundle, plus a README note on browser usage. The approach is clean and well-evidenced — the test exercises the same specifier-level mapping consumers' bundlers use and fails without the fix.

Few things worth tightening:

  • The new esbuild dev dependency ships an install script that runs during normal developer and CI installs, but it's only needed for this one test. Consider a bundler without a lifecycle install hook to stay within the repo's dependency-security requirements.

I also left one optional follow-up note in the details below.

Optional follow-up note (1)
  • P3 src/browser-bundle.test.ts:28: esbuild.build() rejects on build errors (the PR's own Build failed with 121 errors failure is that rejection), so once result is assigned, result.errors is always [] and this assertion can never fail. Drop it; the rejection already fails the test on build errors, and the undiciInputs check below is the assertion that actually guards the fix.

Share Feedback • Review Logs • Trace

Comment thread package-lock.json Resolved
esbuild runs an install script on every npm install, which the
repository's dependency rules don't allow for a new dependency, and it
was only here for the browser-bundle test. rolldown, already installed
for Vite, has none, so the test uses it instead, pinned to the version
the lockfile already resolves.

rolldown doesn't fail a build on an import it can't resolve for a
browser; it logs UNRESOLVED_IMPORT. The test now collects those and
asserts there are none, beside the check that no undici file is
bundled. That replaces the assertion on esbuild's errors, which could
never fail, since esbuild throws on build errors.
@scottlovegrove scottlovegrove added the 🚢 Ship PR requires passing checks before merge, but no independent human review. label Nov 14, 2026
@scottlovegrove
scottlovegrove merged commit 1cbf34f into main Nov 14, 2026
6 checks passed
@scottlovegrove
scottlovegrove deleted the scottlovegrove/browser-bundle branch November 14, 2026 21:58
doist-release-bot Bot added a commit that referenced this pull request Nov 14, 2026
## [5.1.1](v5.1.0...v5.1.1) (2026-11-14)

### Bug Fixes

* let browser bundlers leave undici out ([#149](#149)) ([1cbf34f](1cbf34f))
@doist-release-bot

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 5.1.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

released 🚢 Ship PR requires passing checks before merge, but no independent human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants