-
Notifications
You must be signed in to change notification settings - Fork 552
Bundle the server's dependencies so the packaged app can start #198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| // Bundle each harness-server entry point into a self-contained ESM file. | ||
| // | ||
| // Why this exists: the packaged app ships ZERO node_modules (see the files: | ||
| // exclusion in electron-builder.yml), so anything the server imports by bare | ||
| // specifier has to be inlined — `tsc` only transpiles, it leaves | ||
| // `import { z } from "zod"` verbatim and the packaged server dies at startup | ||
| // with ERR_MODULE_NOT_FOUND. That shipped once, in 0.1.24. | ||
| // | ||
| // Bundling every entry point rather than only index.ts is deliberate: the | ||
| // proxies are spawned as their own processes and today import nothing from | ||
| // node_modules, but nothing stops the next one from doing so, and the failure | ||
| // is invisible until a packaged build is actually launched. | ||
| // | ||
| // Entry points must keep their exact relative paths under dist-server — the | ||
| // server locates each proxy by path (server/index.ts:108, | ||
| // container-computer.ts:773, drivers/acp/core.ts:43), preferring the .ts in | ||
| // dev and falling back to the sibling .js in the packaged tree. outbase keeps | ||
| // drivers/ nested; import.meta.url still resolves to the same location, so | ||
| // that lookup is unaffected. | ||
| import { build } from "esbuild"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import { dirname, join } from "node:path"; | ||
|
|
||
| const root = join(dirname(fileURLToPath(import.meta.url)), ".."); | ||
| const server = join(root, "server"); | ||
|
|
||
| // Every file run as its own process. Keep in sync with the spawn sites above. | ||
| const ENTRY_POINTS = [ | ||
| "index.ts", | ||
| "computer-proxy.ts", | ||
| "container-mcp.ts", | ||
| "permission-proxy.ts", | ||
| "drivers/agents-proxy.ts", | ||
| "drivers/dweb-proxy.ts", | ||
| ]; | ||
|
|
||
| await build({ | ||
| entryPoints: ENTRY_POINTS.map((entry) => join(server, entry)), | ||
| bundle: true, | ||
| platform: "node", | ||
| target: "node20", | ||
| format: "esm", | ||
| outbase: server, | ||
| outdir: join(root, "dist-server"), | ||
| // Written after tsc, replacing its output for these entry points. | ||
| allowOverwrite: true, | ||
| logLevel: "info", | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| // Prove the built server actually STARTS with no node_modules in reach. | ||
| // | ||
| // 0.1.24 shipped a server that died on every launch with | ||
| // ERR_MODULE_NOT_FOUND: Cannot find package 'zod' | ||
| // because `tsc` leaves bare imports verbatim and the packaged app carries no | ||
| // node_modules. Every existing gate passed it: the unit suite runs in the repo | ||
| // (where zod resolves), and the packaging check only asserts index.js EXISTS. | ||
| // | ||
| // So this copies dist-server OUT of the repo before running it. Inside the | ||
| // repo a bare import still resolves by walking up to ./node_modules and the | ||
| // test passes on a build that would be dead in the field — which is precisely | ||
| // how the bug escaped. The copy is the whole point; do not "simplify" it away. | ||
| import { spawn } from "node:child_process"; | ||
| import { cpSync, mkdtempSync, rmSync } from "node:fs"; | ||
| import { tmpdir } from "node:os"; | ||
| import { dirname, join } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
|
|
||
| const root = join(dirname(fileURLToPath(import.meta.url)), ".."); | ||
| const staging = mkdtempSync(join(tmpdir(), "omb-smoke-")); | ||
| const home = mkdtempSync(join(tmpdir(), "omb-smoke-home-")); | ||
| const port = 21000 + Math.floor(Math.random() * 9000); | ||
|
|
||
| cpSync(join(root, "dist-server"), join(staging, "server"), { recursive: true }); | ||
|
|
||
| const child = spawn(process.execPath, [join(staging, "server", "index.js")], { | ||
| cwd: staging, | ||
| env: { | ||
| ...(process.env.PATH ? { PATH: process.env.PATH } : {}), | ||
| ...(process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {}), | ||
| HOME: home, | ||
| USERPROFILE: home, | ||
| OMB_PORT: String(port), | ||
| }, | ||
| stdio: ["ignore", "pipe", "pipe"], | ||
| }); | ||
|
|
||
| let output = ""; | ||
| child.stdout.on("data", (chunk) => (output += chunk)); | ||
| child.stderr.on("data", (chunk) => (output += chunk)); | ||
|
|
||
| // Best-effort by design. Windows holds file handles open a little longer than | ||
| // the process that owned them, so removing the scratch dir immediately after | ||
| // the kill raises EPERM; Linux runners can raise EACCES the same way. Scratch | ||
| // cleanup must never decide whether the build is good — it failed a green run | ||
| // on Windows once already, and see f66d30f for the same lesson on Linux. | ||
| const cleanup = () => { | ||
| child.kill("SIGKILL"); | ||
| for (const dir of [staging, home]) { | ||
| try { | ||
| rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }); | ||
| } catch { | ||
| /* the OS will reap it; the assertion below is what matters */ | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| const deadline = Date.now() + 45_000; | ||
| let listening = false; | ||
| while (Date.now() < deadline) { | ||
| if (child.exitCode !== null) break; | ||
| try { | ||
| const res = await fetch(`http://127.0.0.1:${port}/api/health`); | ||
| if (res.ok) { | ||
| listening = true; | ||
| break; | ||
| } | ||
| } catch { | ||
| /* not up yet */ | ||
| } | ||
| await new Promise((resolve) => setTimeout(resolve, 300)); | ||
| } | ||
|
|
||
| cleanup(); | ||
|
|
||
| if (!listening) { | ||
| console.error(`the packaged server never served /api/health on port ${port}.`); | ||
| console.error(`exit code: ${child.exitCode}`); | ||
| console.error(output.trim() || "(no output)"); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| console.log(`packaged server started with no node_modules in reach (port ${port}) ✓`); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.