From c97659e3428be469deba5e9d3c7f0523de979655 Mon Sep 17 00:00:00 2001 From: deepagent-ai Date: Sat, 1 Aug 2026 11:33:05 +0800 Subject: [PATCH] fix(desktop): externalize server bundle from renderer build (#100) ### Issue for this PR Closes # ### Type of change - [ ] Bug fix - [ ] New feature - [ ] Refactor / code improvement - [ ] Documentation ### What does this PR do? Please provide a description of the issue, the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the PR. **If you paste a large clearly AI generated description here your PR may be IGNORED or CLOSED!** ### How did you verify your code works? ### Screenshots / recordings _If this is a UI change, please include a screenshot or recording._ ### Checklist - [ ] I have tested my changes locally - [ ] I have not included unrelated changes in this PR _If you do not follow this template your PR will be automatically rejected._ --- .github/workflows/desktop-build.yml | 8 ++--- bun.lock | 3 +- packages/deepagent-code/script/build-node.ts | 8 ++++- packages/desktop/electron.vite.config.ts | 33 ++++++++--------- packages/desktop/package.json | 5 +-- .../desktop/scripts/audit-server-bundle.ts | 35 +++++++++++++++++++ 6 files changed, 63 insertions(+), 29 deletions(-) create mode 100644 packages/desktop/scripts/audit-server-bundle.ts diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 7db7924a..74f35a64 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -116,16 +116,12 @@ jobs: p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }} p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - - name: Prebuild - working-directory: packages/desktop - env: - DEEPAGENT_CODE_CHANNEL: ${{ github.event_name == 'push' && 'prod' || (github.event.inputs.channel || 'prod') }} - run: bun run prebuild - + # bun run build invokes the package prebuild lifecycle before electron-vite. - name: Build renderer working-directory: packages/desktop env: DEEPAGENT_CODE_CHANNEL: ${{ github.event_name == 'push' && 'prod' || (github.event.inputs.channel || 'prod') }} + NODE_OPTIONS: --max-old-space-size=4096 run: bun run build - name: Package diff --git a/bun.lock b/bun.lock index 02af02fa..bbbb4deb 100644 --- a/bun.lock +++ b/bun.lock @@ -350,6 +350,7 @@ "version": "1.4.4", "dependencies": { "@deepagent-code/core": "workspace:*", + "@lydell/node-pty": "catalog:", "@zip.js/zip.js": "2.7.62", "effect": "catalog:", "electron-context-menu": "4.1.2", @@ -357,6 +358,7 @@ "electron-store": "^10", "electron-updater": "^6", "electron-window-state": "^5.0.3", + "jsonc-parser": "3.3.1", "marked": "^15", }, "devDependencies": { @@ -364,7 +366,6 @@ "@deepagent-code/app": "workspace:*", "@deepagent-code/ui": "workspace:*", "@jridgewell/trace-mapping": "0.3.31", - "@lydell/node-pty": "catalog:", "@playwright/test": "catalog:", "@sentry/solid": "catalog:", "@sentry/vite-plugin": "catalog:", diff --git a/packages/deepagent-code/script/build-node.ts b/packages/deepagent-code/script/build-node.ts index e5dbf12a..fe65391e 100755 --- a/packages/deepagent-code/script/build-node.ts +++ b/packages/deepagent-code/script/build-node.ts @@ -18,7 +18,7 @@ const result = await Bun.build({ outdir: "./dist/node", format: "esm", sourcemap: "linked", - external: ["jsonc-parser", "@lydell/node-pty"], + external: ["@lydell/node-pty", "jsonc-parser"], define: { DEEPAGENT_CODE_MODELS_DEV: generated.modelsData, DEEPAGENT_CODE_CHANNEL: `'${Script.channel}'`, @@ -43,6 +43,12 @@ const unsupportedBunAPIs = [...new Set(source.match(/\bBun\.[A-Za-z_$][A-Za-z0-9 if (unsupportedBunAPIs.length > 0) { throw new Error(`Node server bundle contains Bun-only runtime APIs: ${unsupportedBunAPIs.join(", ")}`) } +if (!source.includes('from "@lydell/node-pty"')) { + throw new Error("Node server bundle does not import the node-pty platform selector") +} +if (!source.includes('from "jsonc-parser"')) { + throw new Error("Node server bundle does not import the external jsonc-parser runtime dependency") +} await Bun.write( bundle, source diff --git a/packages/desktop/electron.vite.config.ts b/packages/desktop/electron.vite.config.ts index 287d2a15..9045594d 100644 --- a/packages/desktop/electron.vite.config.ts +++ b/packages/desktop/electron.vite.config.ts @@ -1,7 +1,7 @@ import { sentryVitePlugin } from "@sentry/vite-plugin" import { defineConfig } from "electron-vite" import appPlugin from "@deepagent-code/app/vite" -import * as fs from "node:fs/promises" +import { copyFile, cp, mkdir, readdir, rm } from "node:fs/promises" const DEEPAGENT_CODE_SERVER_DIST = "../deepagent-code/dist/node" const DOMAIN_PACKS_DIST = "../domain-packs" @@ -13,8 +13,6 @@ const channel = (() => { return "dev" })() -const nodePtyPkg = `@lydell/node-pty-${process.platform}-${process.arch}` - const sentry = process.env.SENTRY_AUTH_TOKEN && process.env.SENTRY_ORG && process.env.SENTRY_PROJECT ? sentryVitePlugin({ @@ -32,7 +30,7 @@ const sentry = }) : false -export default defineConfig({ +export default defineConfig(({ command }) => ({ main: { define: { "import.meta.env.DEEPAGENT_CODE_CHANNEL": JSON.stringify(channel), @@ -41,32 +39,29 @@ export default defineConfig({ rollupOptions: { input: { index: "src/main/index.ts", sidecar: "src/main/sidecar.ts" }, }, - externalizeDeps: { include: [nodePtyPkg] }, + externalizeDeps: { include: ["@lydell/node-pty"] }, }, plugins: [ - { - name: "deepagent-code:node-pty-narrower", - enforce: "pre", - resolveId(s) { - if (s === "@lydell/node-pty") return nodePtyPkg - }, - }, { name: "deepagent-code:virtual-server-module", enforce: "pre", resolveId(id) { - if (id === "virtual:deepagent-code-server") return this.resolve(`${DEEPAGENT_CODE_SERVER_DIST}/node.js`) + if (id !== "virtual:deepagent-code-server") return + if (command === "build") return { id: "./chunks/node.js", external: true } + return this.resolve(`${DEEPAGENT_CODE_SERVER_DIST}/node.js`) }, }, { name: "deepagent-code:copy-server-assets", async writeBundle() { - for (const l of await fs.readdir(DEEPAGENT_CODE_SERVER_DIST)) { - if (!l.endsWith(".wasm")) continue - await fs.writeFile(`./out/main/chunks/${l}`, await fs.readFile(`${DEEPAGENT_CODE_SERVER_DIST}/${l}`)) + await mkdir("./out/main/chunks", { recursive: true }) + for (const file of await readdir(DEEPAGENT_CODE_SERVER_DIST)) { + if (!file.endsWith(".wasm") && (command !== "build" || (file !== "node.js" && file !== "node.js.map"))) + continue + await copyFile(`${DEEPAGENT_CODE_SERVER_DIST}/${file}`, `./out/main/chunks/${file}`) } - await fs.rm("./out/main/domain-packs", { recursive: true, force: true }) - await fs.cp(DOMAIN_PACKS_DIST, "./out/main/domain-packs", { recursive: true }) + await rm("./out/main/domain-packs", { recursive: true, force: true }) + await cp(DOMAIN_PACKS_DIST, "./out/main/domain-packs", { recursive: true }) }, }, ], @@ -95,4 +90,4 @@ export default defineConfig({ }, }, }, -}) +})) diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 5cacf69f..9c7497bb 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -14,7 +14,7 @@ "predev": "bun ./scripts/predev.ts", "dev": "electron-vite dev", "prebuild": "bun ./scripts/prebuild.ts", - "build": "electron-vite build", + "build": "electron-vite build && bun ./scripts/audit-server-bundle.ts", "preview": "electron-vite preview", "test": "bun test ./src", "test:ci": "mkdir -p .artifacts/unit && bun test ./src --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml", @@ -37,6 +37,7 @@ "main": "./out/main/index.js", "dependencies": { "@deepagent-code/core": "workspace:*", + "@lydell/node-pty": "catalog:", "@zip.js/zip.js": "2.7.62", "effect": "catalog:", "electron-context-menu": "4.1.2", @@ -44,11 +45,11 @@ "electron-store": "^10", "electron-updater": "^6", "electron-window-state": "^5.0.3", + "jsonc-parser": "3.3.1", "marked": "^15" }, "devDependencies": { "@actions/artifact": "4.0.0", - "@lydell/node-pty": "catalog:", "@deepagent-code/app": "workspace:*", "@deepagent-code/ui": "workspace:*", "@jridgewell/trace-mapping": "0.3.31", diff --git a/packages/desktop/scripts/audit-server-bundle.ts b/packages/desktop/scripts/audit-server-bundle.ts new file mode 100644 index 00000000..da8e9043 --- /dev/null +++ b/packages/desktop/scripts/audit-server-bundle.ts @@ -0,0 +1,35 @@ +#!/usr/bin/env bun + +import { strict as assert } from "node:assert" +import { readdir } from "node:fs/promises" +import path from "node:path" + +const chunks = path.resolve("out/main/chunks") +const sidecar = await Bun.file(path.resolve("out/main/sidecar.js")).text() +const server = Bun.file(path.join(chunks, "node.js")) +const sourceMap = Bun.file(path.join(chunks, "node.js.map")) +const files = await readdir(chunks) + +assert.match(sidecar, /import\(["']\.\/chunks\/node\.js["']\)/, "sidecar must load the external server bundle") +assert.equal(await server.exists(), true, "external server bundle is missing") +assert.equal(await sourceMap.exists(), true, "external server source map is missing") +assert.deepEqual( + files.filter((file) => /^node-.+\.js$/.test(file)), + [], + "Rollup must not emit a transformed copy of the server bundle", +) + +const source = await server.text() +assert.match(source, /sourceMappingURL=node\.js\.map/, "external server bundle is not linked to its source map") +assert.equal( + source.includes('from "@lydell/node-pty"'), + true, + "external server bundle does not use the packaged node-pty platform selector", +) +assert.equal( + source.includes('from "jsonc-parser"') || source.includes("from 'jsonc-parser'"), + true, + "external server bundle does not declare its packaged jsonc-parser dependency", +) + +console.log(`External server bundle verified: ${Math.ceil(server.size / 1024 / 1024)} MiB`)