Skip to content

support stagehand in backend functions - #664

Merged
talge-a11y merged 3 commits into
mainfrom
feat/stagehand-workers-compat
Oct 15, 2026
Merged

talge-a11y merged 3 commits into
mainfrom
feat/stagehand-workers-compat

Conversation

@talge-a11y

@talge-a11y talge-a11y commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Note

Description

Closes two workerd compatibility gaps in @base44/functions-compiler that prevented Node-targeted npm packages — Stagehand in particular — from running inside Base44 backend functions. Bare ws / npm:ws now resolves to a ws-shaped client built on workerd's native WebSocket (with a fetch-upgrade path when the caller passes custom handshake headers), and AsyncLocalStorage.prototype.enterWith() is replaced with a no-op when workerd's throwing stub is detected. Both fixes apply to every compiled function and actor, so npm:@browserbasehq/stagehand can be imported, constructed, and driven from a backend function.

Related Issue

None

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • Other (please describe):

Changes Made

  • New esbuild plugin src/esbuild/ws-native-virtual.ts — resolves bare ws and npm:ws[@version] (from any namespace: user files and dependencies) to a single virtual CommonJS module implementing a ws-compatible client over workerd's global WebSocket: on/once/off/addListener/removeListener/removeAllListeners/emit, send, close, terminate, readyState, url, protocol, and the CONNECTING/OPEN/CLOSING/CLOSED constants. Messages are delivered as Buffer with the isBinary flag, matching ws's event signature. ws/lib/* subpaths are deliberately left to resolve normally.
  • Custom handshake headers are honored — the native WebSocket constructor cannot send request headers, so when options.headers carries anything beyond User-Agent (Stagehand's cdpHeaders, e.g. a remote browser's auth token) the socket is opened via a fetch upgrade instead, which carries the headers to the server and follows the function's egress routing. A lone User-Agent stays on the native path.
  • Close semantics match ws within what workerd allows — close codes pass through unchanged (invalid ones throw as in Node), close() during an in-flight upgrade is deferred until the socket lands, and terminate() emits a single 1006 close immediately (workerd has no abrupt close) while suppressing the server's close reply and any open from a pending upgrade. Repeated terminate() calls and terminate() after close are no-ops.
  • WebSocketServer throws a clear error explaining that a function can open WebSocket connections but cannot listen for them.
  • Plugin registered first in deno-bundle.ts, ahead of nodeBuiltinRequirePlugin() and the Deno resolver, so it claims the ws import before the package's browser-condition stub (which throws on use) or its node:net-dependent Node build can be picked.
  • src/shim/entry.ts — probes AsyncLocalStorage#enterWith() on a throwaway instance and, only if it throws, patches the prototype with a no-op, so a future native workerd implementation is left in place. Libraries that call it also pass their context explicitly and re-enter it via run().
  • Docs — new "Workers compatibility for npm clients" section in packages/functions-compiler/README.md documenting both gaps and the adapter's limits (covers what Stagehand uses, not the whole ws API; other connection options ignored; no server support).

Testing

  • I have tested these changes locally
  • I have added/updated tests as needed
  • All tests pass (npm test)

Tests added in packages/functions-compiler/test:

  • workerd-runtime.e2e.test.ts — a parameterized end-to-end case that stands up a Miniflare echo server, bundles a function importing npm:ws@8.18.0, and asserts the round-trip plus a clean close(1000) over both the native-constructor path and the header-carrying fetch-upgrade path (verifying the Authorization header reaches the server). A ws: closing suite covers close-code pass-through and rejection, terminate() emitting one 1006, terminate() during an upgrade never firing open, and repeated/post-close terminate() being a no-op. A final case asserts AsyncLocalStorage.enterWith() is a no-op rather than a throw.
  • package-matrix-data.ts — adds @browserbasehq/stagehand@3.7.3 as an expected: "pass" case in the package matrix, exercising both fixes (construction calls enterWith(), loading imports ws).

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (if applicable)
  • My changes generate no new warnings
  • I have updated docs/ (AGENTS.md) if I made architectural changes

Additional Notes

The ws adapter is client-only and scoped to what Stagehand exercises, not the full ws surface. Connection options other than protocols and headers (agents, TLS settings, per-message deflate) are silently ignored, since neither workerd's native WebSocket nor the fetch upgrade accepts them. The package-matrix and workerd e2e suites pull real packages from npm and run Miniflare, so they need network access to execute.


🤖 Generated by Claude | 2026-10-15 09:05 UTC | f359bf9

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

🚀 Package Preview Available!


Install this PR's preview build with npm:

npm i @base44-preview/cli@0.1.32-pr.664.f359bf9

Prefer not to change any import paths? Install using npm alias so your code still imports base44:

npm i "base44@npm:@base44-preview/cli@0.1.32-pr.664.f359bf9"

Or add it to your package.json dependencies:

{
  "dependencies": {
    "base44": "npm:@base44-preview/cli@0.1.32-pr.664.f359bf9"
  }
}

Preview published to npm registry — try new features instantly!

@talge-a11y
talge-a11y merged commit 88fbd5e into main Oct 15, 2026
15 of 16 checks passed
@talge-a11y
talge-a11y deleted the feat/stagehand-workers-compat branch October 15, 2026 10:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants