#!/usr/bin/env bash # # Bun-compatibility fixups applied to installed dependencies. Runs AFTER `npm ci` # (it edits node_modules) and BEFORE `bun build --compile`. # set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT/httptoolkit-server" # TypeScript's UMD emit wraps the module body in a factory that takes `require` # as a *parameter*: # # })(function (require, exports) { # var bplist_creator_1 = __importDefault(require("bplist-creator")); # # That parameter shadows the module-scope `require`, so bun's bundler can't # statically link the call and leaves it as a runtime require. In a --compile'd # binary there is no node_modules to fall back on, so the import fails at startup # ("Cannot find package 'bplist-creator'"). Note that merely forcing the package # into the bundle does NOT help: bun's runtime require only resolves call sites # it linked at build time. # # Renaming the parameter un-shadows `require`, so the calls bind to the # module-scope require that bun does link. The CJS branch still passes the real # require in as the (now unused) first argument, so behaviour is unchanged. deshadow_umd_require() { local dir="$1" found=0 already=0 for file in "$dir"/*.js; do [ -e "$file" ] || continue if grep -q '})(function (require, exports) {' "$file"; then sed -i 's/})(function (require, exports) {/})(function (__umdRequire, exports) {/' "$file" found=$((found + 1)) elif grep -q '__umdRequire' "$file"; then already=$((already + 1)) fi done echo " - $dir: patched $found, already patched $already" # Idempotent: re-running over an existing tree is fine. Only a tree with # neither form means the dependency changed shape under us. [ $((found + already)) -gt 0 ] \ || { echo "!! no UMD wrappers found in $dir (dependency changed?)"; exit 1; } } echo "==> [node_modules] de-shadowing UMD require() calls for bun" # Reached via @httptoolkit/browser-launcher -> simple-plist (darwin browser # detection). Bun bundles every platform branch regardless of target, so this # has to resolve even in a Linux build. deshadow_umd_require node_modules/simple-plist/dist # Native .node addons only get embedded into the compiled binary if bun can # resolve the require statically. node-datachannel (via mockrtc) instead builds a # require with module.createRequire(): # # const require$1 = module$1.createRequire(pathToFileURL(__filename).href); # const nodeDataChannel = require$1('../build/Release/node_datachannel.node'); # # bun can't see through that, so the addon is left out and the binary resolves it # relative to the *build-time* __filename at runtime - working on the build # machine while failing everywhere else with "Cannot find module # '../build/Release/node_datachannel.node'". Using a plain literal require makes # bun embed the addon as an asset instead. replace_literal() { local file="$1" from="$2" to="$3" [ -f "$file" ] || { echo "!! expected $file to exist (dependency changed?)"; exit 1; } if ! grep -qF "$from" "$file"; then # Idempotent: already-applied is fine, a missing target is not. if grep -qF "$to" "$file"; then echo " - $file: already patched" return 0 fi echo "!! pattern not found in $file: $from"; exit 1 fi python3 - "$file" "$from" "$to" <<'PY' import sys path, old, new = sys.argv[1], sys.argv[2], sys.argv[3] with open(path) as f: content = f.read() with open(path, 'w') as f: f.write(content.replace(old, new)) PY echo " - $file: $from -> $to" } echo "==> [node_modules] making native addon requires statically resolvable" replace_literal node_modules/node-datachannel/lib/index.cjs \ "require\$1('../build/Release/node_datachannel.node')" \ "require('../build/Release/node_datachannel.node')" # bun ships its own built-in `ws` shim and resolves the bare specifier "ws" to it, # shadowing the real npm package. That shim has no createWebSocketStream (and no # Receiver): it throws "Not supported yet in Bun". mockttp's admin server calls # Ws.createWebSocketStream() for every admin client connection, so the server # process died the moment the UI opened a session - after the UI had already # loaded and looked fine. # # Requiring a subpath instead of the bare name escapes bun's shim and gets the # real ws@8, which implements all of this. ws' exports map doesn't publish # ./index.js, so add it before pointing anything at it. redirect_ws_to_real_package() { local dir="$1" count=0 # Publish the subpath we're about to require: if [ "$(jq -r '.exports["./index.js"] // "none"' node_modules/ws/package.json)" = "none" ]; then jq '.exports["./index.js"] = "./index.js"' node_modules/ws/package.json > node_modules/ws/package.json.tmp mv node_modules/ws/package.json.tmp node_modules/ws/package.json echo " - ws/package.json: exposed ./index.js subpath" else echo " - ws/package.json: subpath already exposed" fi while IFS= read -r file; do sed -i 's|require("ws")|require("ws/index.js")|g' "$file" echo " - ${file#node_modules/}: -> ws/index.js" count=$((count + 1)) done < <(grep -rl 'require("ws")' "$dir" --include=*.js 2>/dev/null || true) local already already=$(grep -rl 'require("ws/index.js")' "$dir" --include=*.js 2>/dev/null | wc -l) [ "$already" -gt 0 ] \ || { echo "!! no bare ws requires found in $dir (dependency changed?)"; exit 1; } } echo "==> [node_modules] redirecting ws requires past bun's built-in shim" redirect_ws_to_real_package node_modules/mockttp/dist echo "==> patch-node-modules.sh complete."