fix(server): bundle Bun platform packages so one effect instance serves CORS - #9118
fix(server): bundle Bun platform packages so one effect instance serves CORS#9118lnieuwenhuis wants to merge 3 commits into
Conversation
…es CORS Since 0.0.34 a Bun-hosted server omits `Access-Control-Allow-Origin` from real GET/POST responses while answering OPTIONS preflight correctly, so a desktop app cannot reach a remote environment on a separate HTTPS origin. Node-hosted servers are fine. Compression and the auth refresh / DPoP / cloud credential headers are broken the same way and for the same reason. The CORS code did not change. The CLI bundling change did. `dist/bin.mjs` inlines `effect`, but `@effect/platform-bun` and `@effect/sql-sqlite-bun` stayed external so the bundler would never have to resolve `bun:sqlite`. Under Bun that external `BunHttpServer` loads a second `effect` from node_modules beside the bundle. Effect keys each request's pre-response handler off a module-level WeakMap in `effect/unstable/http/internal/preResponseHandler`: `HttpMiddleware.cors` and `compression` write to it, and the platform server's `toHandled` reads it when sending the response. With two `effect` instances the bundled copy writes handlers the node_modules copy never reads. Preflight survives because `cors` answers OPTIONS inline without touching the WeakMap. `@effect/platform-node` is bundled into the same graph, which is why Node never saw this. Externalize Bun's own module namespace (`bun`, `bun:*`) instead of the packages that import it. That is all the bundler could not resolve, and nothing has to resolve it under Node either: every Bun import sits behind a `typeof Bun !== "undefined"` dynamic import, so it lands in a chunk only a Bun-hosted server loads. The two packages also leave `dependencies`, since nothing resolves them at runtime any more. That trades one silent failure for another, so it is now checked. Inlining moves `bun:sqlite` into the bundle, and it is only harmless while its chunk stays reachable solely through `import()`; statically reachable, every `node bin.mjs` dies with ERR_UNSUPPORTED_ESM_URL_SCHEME. Rolldown merges a dynamic import into its importer with an INEFFECTIVE_DYNAMIC_IMPORT warning and exit 0, and removing these packages from `dependencies` is not a backstop either — the desktop self-containment probe runs `node bin.mjs --version` and never takes the Bun branch. So `assertNoEagerBunImports` in `apps/server/scripts/cli.ts` walks the emitted chunk graph from `bin.mjs` and `service-launcher.mjs` following static edges only and fails the build on any Bun specifier in that set. It runs on every PR through `vp run build:desktop`, unlike the desktop probe. Verified by bundling one probe that mirrors `server.ts`'s conditional `BunHttpServer` import plus `http.ts`'s cors and compression middleware, built both ways and run under Bun 1.2.15. Packages external: no `access-control-allow-origin`, no `content-encoding`, 9728 bytes. Packages bundled: `access-control-allow-origin: *`, `content-encoding: gzip`, 81 bytes. Preflight identical in both. The shipped bundle has no bare `@effect/*-bun` import left, keeps one `bun:sqlite` external in the chunk Node never loads, and defines the pre-response WeakMap exactly once. The real CLI still serves CORS and sets `vary: Accept-Encoding` under Node. Bundle JS grows 8,662,684 -> 8,705,534 bytes while `bin.mjs` itself drops 180,952; the npm install loses 537 KB of now-unused packages. The new check passes on a real build (11 eagerly loaded chunks, no Bun modules). Adding a static `@effect/sql-sqlite-bun/SqliteClient` import to `bin.ts` made it report `bin.mjs -> bun:sqlite` and exit 1, and the bundle it rejected does fail under Node with ERR_UNSUPPORTED_ESM_URL_SCHEME. `vp test run scripts/lib/cli-external-packages.test.ts` (20) passes and `apps/server` typecheck is clean. `scripts/build-desktop-artifact.test.ts` has 7 failures on Windows (symlink EPERM, WSL archive, macOS entitlements) that reproduce identically on an unmodified tree.
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — The change alters the production Bun bundle and restores CORS, compression, and authentication-cookie behavior while adding build-time graph validation and a cross-runtime smoke test. The new test also adds file-level static-analysis diagnostic suppressions, making the overall change require closer review. Not approved because:
Review your spending limits in Billing settings. You can add or adjust custom eligibility rules. Learn more. |
|
@juliusmarminge re your note on #8878 about browser-session cookie emission: here is a manual Linux check of this branch that includes it. Repo-built
The This is symptom-level evidence from the repo build with workspace |
…-instance # Conflicts: # apps/server/package.json # pnpm-lock.yaml
|
A Bun runtime smoke for the browser-session cookie case discussed in #8878 is available here: derektrimm@a90d0ef. It runs the built CLI under Bun 1.4.0 in the Check job and asserts CORS, gzip, and the pairing |
Since the CLI bundling change in #5877,
dist/bin.mjsinlineseffectbut kept@effect/platform-bunand@effect/sql-sqlite-bunexternal. A Bun-hosted server therefore loaded a secondeffectfrom node_modules. Effect attaches CORS, compression, and auth headers to real responses through a pre-response handler stored in a module-level WeakMap, so the bundled copy wrote handlers the platform server's copy never read. OPTIONS preflight still worked because it is answered inline, but every actual response lostAccess-Control-Allow-Originand gzip, which is why the desktop app could not connect to a remote Bun-run environment on a separate origin.The fix externalizes only Bun's own module namespace (
bun,bun:*) and bundles both platform packages, so oneeffectgraph serves the whole server. The two packages move to devDependencies. Because correctness now depends on rolldown keepingbun:sqlitein a chunk reached only throughimport(), the server build gains a post-build check that walks the emitted chunk graph from both entries along static edges and fails if any eagerly loaded chunk imports a Bun module. Rolldown only warns on that merge and exits 0, so the check reads the artifact instead of trusting the bundler.Verification: in the built artifact the single pre-response-handler WeakMap lives in the shared chunk that both
bin.mjsand theBunHttpServerchunk import, and no bare@effect/*-bunruntime import remains. A probe bundled with the repo's own build predicates and run under Bun showedAccess-Control-Allow-Origin: *andContent-Encoding: gziprestored on GET, and the real CLI under Node shows no regression. Bundle size grows by about 0.5%. The full server cannot run under Bun on Windows (the Bun PTY adapter is unsupported there), and the freshly built CLI has now passed the Linux Bun runtime smoke described below. A negative control confirmed the new build check rejects a bundle wherebun:sqliteis hoisted intobin.mjs, which otherwise fails at load under Node.Closes #8878
Claude Fable 5.1 via Claude Code
Fresh verification (2026-09-06): built the server/web artifacts and passed the isolated Linux Bun 1.4.0 runtime smoke for CORS, gzip body equivalence, pairing Set-Cookie, and authenticated cookie replay. The smoke is now wired into CI. 20 focused packaging tests passed. Published-package installation was not exercised; Windows Bun startup still has the existing unsupported PTY limitation.
Note
Medium Risk
Changes core server CLI bundling and build-time validation; a mistaken static import of Bun adapters would fail builds or break Node entrypoints, but the new eager-import scan is meant to catch that before release.
Overview
Fixes missing CORS, gzip, and auth on Bun-hosted servers when the desktop app talks to a remote environment on another origin. External
@effect/platform-bunand@effect/sql-sqlite-bunhad been loading a secondeffectbeside the bundle, so pre-response handlers (WeakMap-keyed) never applied to real responses.Bundling policy in
cli-external-packages.tsnow inlines those two packages and only keepsbun/bun:*external. They move fromdependenciesto devDependencies inapps/server/package.json. Inlining pullsbun:sqliteinto the artifact, which must stay behind dynamicimport()sonode bin.mjsstill works.A post-bundle guard runs in the server CLI
buildstep:findEagerBunRuntimeImportswalks static edges frombin.mjsandservice-launcher.mjsand fails withServerCliEagerBunImportErrorif any eagerly loaded chunk references a Bun runtime module (rolldown only warns). Tests cover the scanner and updated bundle expectations; desktop self-containment comments point at this check for the Bun adapters.Reviewed by Cursor Bugbot for commit bff7b06. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Bundle
@effect/platform-bunand@effect/sql-sqlite-buninstead of externalizing them@effect/platform-bunand@effect/sql-sqlite-bunfrom CLI external prefixes in cli-external-packages.ts so they are bundled rather than left external. Bun runtime modules (bun,bun:*) remain external.findEagerBunRuntimeImportsto walk the emitted chunks' static import graph from entry chunks and report any reachable Bun runtime module imports.assertNoEagerBunImportsto the build command in cli.ts, which fails the build withServerCliEagerBunImportErrorif any eagerly loaded chunk imports a Bun runtime module.isExternalCliDependencynow bundles@effect/platform-bunand@effect/sql-sqlite-bun; if any code statically imports Bun runtime modules through these packages, the build will fail viaassertNoEagerBunImportsrather than silently producing a broken bundle.📊 Macroscope summarized bff7b06. 4 files reviewed, 1 issue evaluated, 1 issue filtered, 0 comments posted
🗂️ Filtered Issues
apps/server/scripts/cliErrors.ts — 0 comments posted, 1 evaluated, 1 filtered
ServerCliEagerBunImportErroralso reports violations for the bare specifierbun(as exercised by the new scanner test), but its message always says the failure isERR_UNSUPPORTED_ESM_URL_SCHEMEfor abun:URL. A static barebunimport is package resolution and Node reportsERR_MODULE_NOT_FOUND, so this build failure gives an incorrect diagnosis for one of the explicitly detected violation types. [ Out of scope (post-validation triage) ]