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`)