150 lines
7.1 KiB
Bash
Executable File
150 lines
7.1 KiB
Bash
Executable File
#!/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
|
|
|
|
# mockttp decides whether a response is still ongoing or already completed by
|
|
# sniffing the object's shape:
|
|
#
|
|
# // Ongoing response has 'getHeaders' - completed has 'headers'.
|
|
# if ('headers' in response) return response;
|
|
#
|
|
# An ongoing response is a live http.ServerResponse; a completed one is the plain
|
|
# object built by buildInitiatedResponse(). On node that test works, because
|
|
# ServerResponse has no `headers` property. Bun's ServerResponse *does* have one
|
|
# (`'headers' in res` === true, typeof object), so the check is inverted for every
|
|
# single response: waitForCompletedResponse() bails out immediately and hands back
|
|
# the live ServerResponse, having neither awaited the body nor attached rawHeaders.
|
|
#
|
|
# The admin server then tries to serialise that as a GraphQL Response, whose
|
|
# schema declares `rawHeaders: Json!`, and the whole responseCompleted event is
|
|
# dropped with "Cannot return null for non-nullable field Response.rawHeaders."
|
|
# Traffic still proxies correctly - but the UI only ever receives the *initiated*
|
|
# response, so exchanges render with status and headers and no body, forever.
|
|
#
|
|
# Testing for the ongoing-response method instead is unambiguous under either
|
|
# runtime: a completed response is a plain object and has no getHeaders.
|
|
echo "==> [node_modules] fixing mockttp's ongoing-vs-completed response check for bun"
|
|
replace_literal node_modules/mockttp/dist/util/request-utils.js \
|
|
"if ('headers' in response)" \
|
|
"if (typeof response.getHeaders !== 'function')"
|
|
|
|
echo "==> patch-node-modules.sh complete."
|