fix: reduce desktop package size and loose files

Exclude validation outputs from desktop packages, ship renderer-only dependencies through Vite bundles, and omit DS Web type declarations, source maps, and PDB symbols. Preserve runtime assets and licenses without changing dependency versions.

Validate builder filters, production build and type checks, Windows packaged runtime smoke paths, and App page loading. Record measured size reductions and remaining validation limits in BUILD.md.

Release note: 减少桌面安装包中的重复依赖和非运行文件,降低安装后的磁盘占用与零散文件数量。
This commit is contained in:
mesalogo
2026-09-28 20:10:19 +08:00
parent 1c6264dd87
commit 594222a2b5
8 changed files with 440 additions and 59 deletions
+24
View File
@@ -182,6 +182,30 @@ npm run dist:linux:arm64
已由 Vite 打包的 `@antv/g6` 归类为构建期依赖,不会再作为独立 Node 模块复制进桌面
安装包。
Desktop packaging includes only `out/main`, `out/preload`, and `out/renderer`;
validation builds elsewhere under `out` are not shipped. Renderer-only React,
Xterm, Mermaid, Fabric, Quill, i18next, and Markdown/math dependencies are build-time
dependencies: Vite ships their browser bundles, while the existing license
resources remain included. Install development dependencies before building;
do not prune them before bundling and copying license resources.
The desktop DS Web resource filter excludes JavaScript/TypeScript source maps,
`.d.ts` / `.d.mts` / `.d.cts` declarations, and `.pdb` debug symbols. It preserves
the installed dependency layout, plugin configuration, browser assets, licenses,
native binaries, and non-code `.map` assets. This filter does not change the
separately built remote Agent or Runtime payloads.
Windows x64 development validation on 2026-09-28 compared the existing 0.15.2
portable directory (excluding `data`) with a fresh unpacked test build:
`app.asar` decreased from 609.4 to 350.5 MiB, and DS Web decreased from 28,701
files / 463.4 MiB to 14,984 files / 357.0 MiB. Total measured size decreased from
1,758.3 to 1,438.4 MiB despite the test build retaining 45.6 MiB more Electron
locales than the portable build. These are unpacked sizes, not installer download
sizes. Builder-copy regression tests, production build/type checks, packaged
Harness/OpenCode/npm smoke checks, DS Web authentication/page scripts/workspace
creation, and packaged App/settings/notes-page loading passed. Full editor/canvas
interaction and other platform packages were not revalidated in this check.
以上 `dist*` 与 `portable` 命令用于普通本地桌面打包。它们会携带
`agent-runtime-lock.json`、`remote-runtime-lock.json` 与公开的
`agent-release-keys.json`,但所有桌面构建路径(包括本地 `portable`)都不会嵌入
+327 -34
View File
File diff suppressed because it is too large Load Diff
+27 -19
View File
@@ -76,14 +76,22 @@
"npmRebuild": false,
"compression": "maximum",
"files": [
"out/**/*",
"out/main/**/*",
"out/preload/**/*",
"out/renderer/**/*",
"package.json",
"!node_modules/npm{,/**/*}"
],
"extraResources": [
{
"from": ".runtime-resources/dsh-${arch}/node_modules",
"to": "runtimes/dsh/node_modules"
"to": "runtimes/dsh/node_modules",
"filter": [
"**/*",
"!**/*.{js,mjs,cjs,ts,mts,cts}.map",
"!**/*.d.{ts,mts,cts}",
"!**/*.pdb"
]
},
{
"from": "resources/skills",
@@ -332,37 +340,20 @@
"@opencode-ai/sdk": "1.18.29",
"@vscode/ripgrep": "1.18.0",
"@wecom/aibot-node-sdk": "^1.0.6",
"@xterm/addon-fit": "0.11.0",
"@xterm/addon-search": "0.16.0",
"@xterm/addon-webgl": "0.19.0",
"@xterm/xterm": "6.0.0",
"cross-spawn": "^7.0.6",
"dingtalk-stream": "^2.1.6-beta.1",
"dompurify": "^3.4.13",
"fflate": "^0.8.3",
"fabric": "7.4.0",
"html-to-text": "^10.0.0",
"i18next": "^25.10.10",
"json5": "^2.2.3",
"jspdf": "4.2.1",
"katex": "^0.16.47",
"koffi": "3.1.4",
"lucide-react": "^1.27.0",
"mermaid": "^11.16.1",
"node-pty": "^1.2.0-beta.15",
"npm": "11.19.0",
"onnxruntime-web": "^1.23.2",
"pdfjs-dist": "^6.2.108",
"ppu-paddle-ocr": "^6.4.0",
"qrcode": "^1.5.4",
"quill": "^2.0.3",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-i18next": "^16.6.6",
"react-markdown": "^10.1.0",
"rehype-katex": "^7.0.1",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"sherpa-onnx": "1.13.4",
"ssh2": "^1.17.0",
"undici": "^7.29.0",
@@ -387,17 +378,34 @@
"@types/react-dom": "^19.2.3",
"@types/ssh2": "^1.15.5",
"@vitejs/plugin-react": "^5.2.0",
"@xterm/addon-fit": "0.11.0",
"@xterm/addon-search": "0.16.0",
"@xterm/addon-webgl": "0.19.0",
"@xterm/xterm": "6.0.0",
"electron": "^43.2.0",
"electron-builder": "^26.15.3",
"electron-vite": "^5.0.0",
"esbuild": "0.25.12",
"eslint": "^10.8.0",
"eslint-plugin-react-hooks": "^7.1.1",
"fabric": "7.4.0",
"globals": "^17.8.0",
"i18next": "^25.10.10",
"jsdom": "^30.0.1",
"katex": "^0.16.47",
"lucide-react": "^1.27.0",
"mermaid": "^11.16.1",
"opencode-ai": "1.18.29",
"png-to-ico": "^3.0.2",
"pngjs": "^7.0.0",
"quill": "^2.0.3",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-i18next": "^16.6.6",
"react-markdown": "^10.1.0",
"rehype-katex": "^7.0.1",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"tar": "7.5.22",
"typescript": "^6.0.3",
"typescript-eslint": "^8.65.0",
+5 -1
View File
@@ -8890,7 +8890,11 @@ function App(): React.JSX.Element {
}}
>
<span className="conversation-item__primary">
{conversation.pinned && <Pin className="conversation-pin" size={13} role="img" aria-label={t("conversation.actions.pinned")}><title>{t("conversation.actions.pinned")}</title></Pin>}
{conversation.pinned && (
<span className="conversation-pin">
<Pin size={13} role="img" aria-label={t("conversation.actions.pinned")}><title>{t("conversation.actions.pinned")}</title></Pin>
</span>
)}
{branchSourceTitle && (
<ConversationBranchBadge
sourceTitle={branchSourceTitle}
+6
View File
@@ -12554,7 +12554,13 @@ textarea.field-control {
}
.conversation-pin {
display: inline-flex;
width: 20px;
height: 18px;
flex: 0 0 auto;
align-items: center;
justify-content: center;
margin-right: var(--space-1);
color: var(--text-secondary);
}
+3 -1
View File
@@ -810,7 +810,9 @@ describe('release build arguments', () => {
)
expect(packageJson.build.files).toEqual([
'out/**/*',
'out/main/**/*',
'out/preload/**/*',
'out/renderer/**/*',
'package.json',
'!node_modules/npm{,/**/*}'
])
+22 -1
View File
@@ -1,5 +1,5 @@
// @vitest-environment node
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
@@ -13,6 +13,27 @@ const { getFileMatchers, copyFiles } = require('app-builder-lib/out/fileMatcher'
const { verifyOpenCodeConfig, verifyHarnessBundleImports } = require('../build/verify-desktop-runtimes.cjs')
describe('desktop Runtime packaging', () => {
it('ships only production output even when validation builds remain in out', async () => {
const root = mkdtempSync(join(tmpdir(), 'goodbuddy-production-output-'))
const output = join(root, 'output')
const included = ['out/main/index.js', 'out/main/chunks/shared.js', 'out/main/obsidian-mcpvault/server.js', 'out/preload/index.cjs', 'out/renderer/index.html', 'package.json']
const excluded = ['out/chat-paste-validation/renderer/index.html', 'out/another-probe/main.js']
try {
for (const file of [...included, ...excluded]) {
mkdirSync(dirname(join(root, file)), { recursive: true })
writeFileSync(join(root, file), file)
}
await copyFiles(getFileMatchers({ files: packageJson.build.files }, 'files', output, {
defaultSrc: root, globalOutDir: output, customBuildOptions: {},
macroExpander: (value: string) => value
}))
for (const file of included) expect(existsSync(join(output, file)), file).toBe(true)
for (const file of excluded) expect(existsSync(join(output, file)), file).toBe(false)
} finally {
rmSync(root, { recursive: true, force: true })
}
})
it('runs packaged Windows Runtime checks in CI before removing the unpacked output', () => {
const release = readFileSync(join(process.cwd(), 'build', 'build-release.cjs'), 'utf8')
const smoke = readFileSync(join(process.cwd(), 'build', 'run-packaged-deepseek-harness-smoke.cjs'), 'utf8')
+26 -3
View File
@@ -1,4 +1,4 @@
import { mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
@@ -12,9 +12,29 @@ it('copies DS dependencies and nested assets through the actual builder matcher'
const root = mkdtempSync(join(tmpdir(), 'goodbuddy-dsh-packaging-'))
const config = JSON.parse(readFileSync(resolve('package.json'), 'utf8'))
const resource = config.build.extraResources.find((entry: { to: string }) => entry.to.startsWith('runtimes/dsh'))
const files = ['@deepseek-ai/dsh/lib/bin.js', '@deepseek-ai/dsh/node_modules/example/index.js', '@deepseek-ai/dsh-web/dist/index.html']
const files = [
'@deepseek-ai/dsh/lib/bin.js',
'@deepseek-ai/dsh/node_modules/example/index.js',
'@deepseek-ai/dsh-web/dist/index.html',
'@deepseek-ai/dsh-web/config.yml',
'@deepseek-ai/dsh-web/LICENSE',
'native/addon.node',
'native/helper.exe',
'native/helper.dll',
'assets/data.map'
]
const excluded = [
'@deepseek-ai/dsh/lib/bin.js.map',
'@deepseek-ai/dsh/node_modules/example/index.d.ts',
'example/index.d.mts',
'example/index.d.cts',
'example/index.d.ts.map',
'example/index.mjs.map',
'example/index.cjs.map',
'native/helper.pdb'
]
try {
for (const file of files) {
for (const file of [...files, ...excluded]) {
const source = join(root, '.runtime-resources/dsh-x64/node_modules', file)
mkdirSync(dirname(source), { recursive: true })
writeFileSync(source, file)
@@ -30,6 +50,9 @@ it('copies DS dependencies and nested assets through the actual builder matcher'
for (const file of files) {
expect(readFileSync(join(destination, 'runtimes/dsh/node_modules', file), 'utf8')).toBe(file)
}
for (const file of excluded) {
expect(existsSync(join(destination, 'runtimes/dsh/node_modules', file)), file).toBe(false)
}
} finally {
rmSync(root, { recursive: true, force: true })
}