feat: migrate from Node SEA to Bun compile#185
Conversation
Replace the 5-tool Node SEA pipeline (ncc, bpkg, sea-config, postject,
codesign) with Bun's --compile which natively embeds N-API addons and
produces smaller binaries (~68MB vs ~106MB).
- Rewrite src/preload.ts: remove node:sea, simplify importNodeDumpSyms
to a plain import, use globalThis for embedded worker path in compiled mode
- Add bin/compile-entry.ts: Bun compile entry point that embeds the
pre-bundled worker via import with { type: "file" }
- Simplify bin/command-line-definitions.ts: replace node:sea with JSON import
- Add resolveJsonModule to tsconfig.json
- Replace SEA build scripts with bundle:worker + build:compile
- Remove devDependencies: @vercel/ncc, bpkg, postject, npm-run-all,
@mapbox/node-pre-gyp
- Delete sea-config.json and sea/ directory
- Rewrite CI workflow for Bun (oven-sh/setup-bun, bun install, bun build)
- Add workerpool patch: replace eval-based requireFoolWebpack with direct
require for Bun compile compatibility
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bun's fetch doesn't support ReadableStreams created via Node's ReadStream.toWeb() as PUT request bodies (duplex streaming issue). Use Bun.file().stream() which produces a proper web-standard ReadableStream that works with Bun's fetch, while keeping ReadStream.toWeb() for Node.js. Both paths are streaming — no buffering the entire file into memory. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- compile-entry.ts: use dynamic import so __embeddedWorkerPath is set before the app starts (static imports are hoisted before module body) - preload.ts: check embeddedPath before devPath to avoid finding the unbundled source file; use readFileSync for /$bunfs/ virtual filesystem - worker.ts: return Bun.file() (lazy Blob) instead of .stream() so fetch sends proper content-length instead of chunked encoding Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SymbolsApiClient.postSymbols tees the body to verify gzip magic bytes, which requires a ReadableStream. Returning Bun.file() (a Blob) broke uploads in the Bun compiled binary with "R.tee is not a function". Content-Length is already set explicitly in s3-api-client headers, so Bun.file().stream() is safe and remains lazy/streaming. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mpile # Conflicts: # package.json
|
Status update: compiled binary now works end-to-end on Bun canary Verified in the compiled binary:
Fix in this round: Blocker before merging: The fix for oven-sh/bun#20740 ( Tests: 44/44 passing locally after merging main. |
Bun 1.3.13 is the first stable release containing the fix for oven-sh/bun#20740 (Bun.file() as fetch body segfault in compiled binaries), which this branch depends on. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bun skips install/postinstall lifecycle scripts by default. node-dump-syms uses its install script to download the prebuilt native .node binary via node-pre-gyp. Without this, fresh bun install on CI or new machines leaves the native binary missing and bun build --compile fails to resolve ../native/index.node. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR migrates the project’s single-binary build from Node SEA to Bun’s bun build --compile, simplifying the build pipeline and adding a workerpool patch to support Bun-compiled binaries.
Changes:
- Replaces the Node SEA toolchain and scripts with a Bun-based compile workflow (
bun build --compile) and updated CI. - Adds a Bun compile entrypoint that embeds the pre-bundled compression worker and exposes its path via
globalThis.__embeddedWorkerPath. - Updates upload streaming to support Bun (via
Bun.file(...).stream()) and adds aworkerpoolpatch to avoideval()-basedrequire.
Reviewed changes
Copilot reviewed 10 out of 15 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
tsconfig.json |
Enables resolveJsonModule to support JSON imports used by updated CLI metadata. |
src/worker.ts |
Switches upload file streaming abstraction to support Bun vs Node runtimes. |
src/preload.ts |
Reworks compression worker path resolution to support Bun compiled asset extraction. |
spec/worker.spec.ts |
Updates tests to match the new stream/destroy abstraction. |
bin/compile-entry.ts |
New Bun compile entrypoint embedding the worker bundle and importing the CLI. |
bin/command-line-definitions.ts |
Simplifies version resolution via package.json import (currently broken for dist output). |
.github/workflows/sea.yaml |
Replaces Node SEA workflow steps with Bun install + bun build --compile. |
patches/workerpool@6.5.1.patch |
Patches workerpool to avoid eval() require indirection for Bun compile. |
package.json |
Removes SEA-related scripts/deps; adds Bun compile scripts and patched dependency metadata. |
package-lock.json |
Updates lockfile to reflect dependency changes (SEA toolchain removals, node-dump-syms bump, etc.). |
bun.lock |
Adds Bun lockfile for Bun-based install/compile flow. |
sea-config.json, sea/*.sh, sea/windows.ps1 |
Removes obsolete Node SEA configuration/scripts. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import { existsSync, mkdirSync, writeFileSync } from "node:fs"; | ||
| import { mkdir, writeFile } from "node:fs/promises"; | ||
| import { createRequire } from "node:module"; | ||
| import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; |
There was a problem hiding this comment.
copyFileSync is imported but never used (only referenced in a comment). Please remove it to avoid dead imports and keep the module surface minimal.
| import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; | |
| import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; |
|
made the binaries smaller on macos and linux but made the windows binary bigger. since windows is our most popular platform this change kind of defeats the purpose. closing for now |
Summary
--compile, which natively embeds N-API.nodeaddons and produces smaller binaries (~68MB vs ~106MB)node:seafrompreload.tsandcommand-line-definitions.ts, simplifying both significantlyoven-sh/setup-bun+bun build --compileworkerpoolto fix aneval()-based require that doesn't work in Bun compiled binariesDetails
New file:
bin/compile-entry.ts— Bun compile entry point that embeds the pre-bundled compression worker viaimport with { type: "file" }and stores its path onglobalThis.__embeddedWorkerPath. This file is excluded fromtscso the npm package path is unaffected.Removed:
sea-config.json,sea/macos.sh,sea/linux.sh,sea/windows.ps1, and devDependencies@vercel/ncc,bpkg,postject,npm-run-all,@mapbox/node-pre-gypworkerpool patch:
workerpooluseseval("typeof require !== 'undefined' ? require : ...")to hiderequirefrom webpack. In Bun's compiled binary,requireisn't available insideeval(), so the patch replaces this with a directmodule.exports = require.Blocked
This PR is blocked by a Bun bug where
Bun.file()as afetch()body segfaults in compiled binaries (works fine withbun run). This affects S3 presigned URL uploads of symbol files.Test plan
tsc --noEmitpasses (npm package path unaffected)vitest run— all 43 tests passbun build --compileproduces working binary./dist/symbol-upload -hshows help with correct version./dist/symbol-upload -m -l ./tmp/local -d ./spec/support -f "**/*.dSYM"exercises native addon loading + dump_syms + file output🤖 Generated with Claude Code