chore(desktop): trim external dependency archives (#45804)

This commit is contained in:
Luke Parker 2026-08-28 12:24:46 +10:00 committed by GitHub
parent 88988b8c12
commit c5113ee74a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 122 additions and 1 deletions

View file

@ -1,7 +1,15 @@
import { expect, test } from "bun:test"
import { statSync } from "node:fs"
import { cp, mkdtemp, rm } from "node:fs/promises"
import { createRequire } from "node:module"
import os from "node:os"
import path from "node:path"
import { fileURLToPath, pathToFileURL } from "node:url"
import type { Configuration } from "electron-builder"
const legacyDesktopEntry = "resources/linux/opencode-desktop.desktop"
// Use electron-builder's matcher so the tests also cover its glob and directory traversal semantics.
const { FileMatcher } = createRequire(import.meta.resolve("electron-builder"))("app-builder-lib/out/fileMatcher")
const channels = [
{ channel: "dev", appId: "ai.opencode.desktop.dev" },
@ -27,8 +35,105 @@ for (const channel of channels) {
expect(config.deb?.fpm).toContainEqual(expect.stringContaining(`/usr/share/metainfo/${channel.appId}.metainfo.xml`))
expect(config.rpm?.fpm).toContainEqual(expect.stringContaining(`/usr/share/metainfo/${channel.appId}.metainfo.xml`))
})
test(`trims external dependencies without excluding runtime files for ${channel.channel}`, async () => {
const config = (await import(`./electron-builder.config.ts?channel=${channel.channel}`)).default as Configuration
const filter = new FileMatcher(import.meta.dirname, "", (value: string) => value, [
"**/*",
...(Array.isArray(config.files) ? config.files : []).filter(
(value): value is string => typeof value === "string" && value.startsWith("!"),
),
]).createFilter()
for (const prefix of ["node_modules/", "node_modules/parent/node_modules/"]) {
for (const file of [
"@zip.js/zip.js/dist/zip.js",
"@zip.js/zip.js/dist/z-worker.js",
"@zip.js/zip.js/index.cjs",
"@zip.js/zip.js/index.min.js",
"@zip.js/zip.js/index-fflate.js",
"@zip.js/zip.js/deno.json",
"@zip.js/zip.js/eslint.config.mjs",
"electron-updater/out/main.js.map",
"electron-updater/out/providers/GitHubProvider.js.map",
"builder-util-runtime/out/httpExecutor.js.map",
"lazy-val/out/main.js.map",
"ajv/lib/core.ts",
"ajv/dist/compile/index.js.map",
"ajv-formats/src/formats.ts",
"ajv-formats/dist/formats.js.map",
"js-yaml/dist/js-yaml.js",
"js-yaml/dist/js-yaml.min.js",
"js-yaml/dist/js-yaml.mjs.map",
"js-yaml/bin/js-yaml.js",
]) {
expect(filter(path.join(import.meta.dirname, prefix, file), statSync(import.meta.filename))).toBe(false)
}
for (const file of [
"@zip.js/zip.js/index.js",
"@zip.js/zip.js/lib/zip-fs.js",
"@zip.js/zip.js/lib/z-worker-inline.js",
"@zip.js/zip.js/lib/core/streams/codecs/deflate.js",
"electron-updater/out/main.js",
"electron-updater/out/MacUpdater.js",
"electron-updater/out/NsisUpdater.js",
"electron-updater/out/providers/GitHubProvider.js",
"builder-util-runtime/out/httpExecutor.js",
"lazy-val/out/main.js",
"ajv/dist/ajv.js",
"ajv/dist/refs/json-schema-draft-07.json",
"ajv-formats/dist/formats.js",
"js-yaml/index.js",
"js-yaml/lib/loader.js",
"js-yaml/dist/js-yaml.mjs",
"debug/src/index.js",
"unrelated/dist/index.js.map",
...["@zip.js/zip.js", "electron-updater", "builder-util-runtime", "ajv", "ajv-formats", "js-yaml"].flatMap(
(name) => [`${name}/package.json`, `${name}/LICENSE`],
),
]) {
expect(filter(path.join(import.meta.dirname, prefix, file), statSync(import.meta.filename))).toBe(true)
}
expect(filter(path.join(import.meta.dirname, prefix, "@zip.js/zip.js/dist"), statSync(import.meta.dirname))).toBe(
false,
)
expect(filter(path.join(import.meta.dirname, prefix, "@zip.js/zip.js/lib"), statSync(import.meta.dirname))).toBe(
true,
)
}
})
}
test("the trimmed Zip.js package can still export compressed logs", async () => {
const config = (await import("./electron-builder.config.ts")).default
const dir = await mkdtemp(path.join(os.tmpdir(), "opencode-zip-package-"))
const source = path.dirname(fileURLToPath(import.meta.resolve("@zip.js/zip.js/package.json")))
const filter = new FileMatcher(dir, "", (value: string) => value, [
"**/*",
...(Array.isArray(config.files) ? config.files : []).filter(
(value): value is string => typeof value === "string" && value.startsWith("!"),
),
]).createFilter()
try {
await cp(source, dir, {
recursive: true,
filter: (file) =>
filter(path.join(dir, "node_modules/@zip.js/zip.js", path.relative(source, file)), statSync(file)),
})
const zip = await import(pathToFileURL(path.join(dir, "index.js")).href)
const writer = new zip.ZipWriter(new zip.BlobWriter("application/zip"))
await writer.add("desktop.log", new zip.BlobReader(new Blob(["diagnostic log\n".repeat(100)])))
const reader = new zip.ZipReader(new zip.BlobReader(await writer.close()))
const entries = await reader.getEntries()
expect(entries.map((entry: { filename: string }) => entry.filename)).toEqual(["desktop.log"])
expect(entries[0].compressionMethod).toBe(8)
expect(await entries[0].getData(new zip.TextWriter())).toBe("diagnostic log\n".repeat(100))
await reader.close()
await zip.terminateWorkers()
} finally {
await rm(dir, { recursive: true, force: true })
}
})
test("keeps a hidden prod launcher for old Linux pins", async () => {
const previous = process.env.OPENCODE_CHANNEL
process.env.OPENCODE_CHANNEL = "prod"

View file

@ -55,7 +55,22 @@ const getBase = (appId: string): Configuration => ({
extraMetadata: {
desktopName: `${appId}.desktop`,
},
files: ["out/**/*", "resources/**/*", "!resources/opencode-cli*"],
files: [
"out/**/*",
"resources/**/*",
"!resources/opencode-cli*",
// Log export imports Zip.js as ESM. Keep index.js and lib, including its inline worker.
"!**/node_modules/@zip.js/zip.js/dist{,/**/*}",
"!**/node_modules/@zip.js/zip.js/{index.cjs,index.min.js,index-fflate.js,deno.json,eslint.config.mjs}",
// These packages execute compiled JavaScript, not their sources or source maps.
"!**/node_modules/{electron-updater,builder-util-runtime,lazy-val}/out/**/*.js.map",
"!**/node_modules/ajv/lib{,/**/*}",
"!**/node_modules/ajv-formats/src{,/**/*}",
"!**/node_modules/{ajv,ajv-formats}/dist/**/*.js.map",
// Keep js-yaml's CommonJS sources and dist/js-yaml.mjs ESM entry, not browser bundles or its CLI.
"!**/node_modules/js-yaml/dist/{js-yaml.js,js-yaml.min.js,*.map}",
"!**/node_modules/js-yaml/bin{,/**/*}",
],
extraResources:
channel !== "prod"
? [

View file

@ -72,6 +72,7 @@ test("bundles one Effect runtime and Drizzle while keeping native dependencies e
expect(new Set(effect).size).toBe(effect.length)
expect(imports).toContain("electron")
expect(imports).toContain("node:sqlite")
expect(chunks.some((chunk) => chunk.dynamicImports.includes("@zip.js/zip.js"))).toBe(true)
expect(imports).toContain(`@lydell/node-pty-${process.platform}-${process.arch}`)
expect(modules.some((id) => id.includes("/node_modules/msgpackr-extract/"))).toBe(false)
expect(chunks.some((chunk) => chunk.code.includes("msgpackr-extract"))).toBe(true)