From 2b551b87f7e0b80bbb4180d4c73b380079a3f28a Mon Sep 17 00:00:00 2001 From: Eva Date: Sat, 27 Jun 2026 18:24:08 +0700 Subject: [PATCH 1/7] Prune renderer dependencies from production manifest --- bun.lock | 74 +++++++++---------- .../workbench-renderer-dependency-ledger.md | 58 +++++++++++++++ package.json | 74 +++++++++---------- 3 files changed, 132 insertions(+), 74 deletions(-) create mode 100644 docs/evaos/workbench-renderer-dependency-ledger.md diff --git a/bun.lock b/bun.lock index e4113048785..1fb5abe9517 100644 --- a/bun.lock +++ b/bun.lock @@ -8,50 +8,30 @@ "@agentclientprotocol/sdk": "^0.18.2", "@aionui/web-host": "workspace:*", "@anthropic-ai/sdk": "^0.71.2", - "@arco-design/web-react": "^2.66.1", "@aws-sdk/client-bedrock": "^3.987.0", - "@codemirror/commands": "^6.8.1", - "@codemirror/lang-css": "^6.3.1", - "@codemirror/lang-html": "^6.4.11", - "@codemirror/lang-json": "^6.0.2", - "@codemirror/lang-markdown": "^6.5.0", - "@codemirror/view": "^6.38.6", - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", - "@floating-ui/react": "^0.27.16", "@google/genai": "^1.16.0", "@grammyjs/transformer-throttler": "^1.2.1", - "@icon-park/react": "^1.4.2", "@larksuiteoapi/node-sdk": "^1.58.0", "@modelcontextprotocol/sdk": "^1.20.0", - "@monaco-editor/react": "^4.7.0", "@office-ai/aioncli-core": "^0.30.6", "@office-ai/platform": "^0.3.16", "@sentry/electron": "^7.10.0", "@types/bcryptjs": "^2.4.6", "@types/jsonwebtoken": "^9.0.10", - "@uiw/codemirror-extensions-langs": "^4.25.1", - "@uiw/react-codemirror": "^4.25.2", "@wecom/aibot-node-sdk": "^1.0.6", "@xmldom/xmldom": "^0.8.11", "bcryptjs": "^2.4.3", "better-sqlite3": "^12.4.1", "buffer": "^6.0.3", - "classnames": "^2.5.1", "cookie": "^1.0.2", "cookie-parser": "^1.4.7", "cors": "^2.8.5", "croner": "^9.1.0", - "dayjs": "^1.11.18", - "diff": "^8.0.3", - "diff2html": "^3.4.55", "dingtalk-stream": "^2.1.4", "docx": "^9.5.1", "electron-log": "^5.4.3", "electron-squirrel-startup": "^1.0.1", "electron-updater": "^6.6.2", - "eventemitter3": "^5.0.1", "express": "^5.1.0", "express-rate-limit": "^7.5.1", "fix-path": "^4.0.0", @@ -61,35 +41,18 @@ "i18next": "^23.7.16", "jsonrepair": "^3.13.0", "jsonwebtoken": "^9.0.2", - "katex": "^0.16.22", "mammoth": "^1.11.0", - "mermaid": "^11.13.0", "multer": "^2.1.1", "officeparser": "^5.2.2", "openai": "^5.12.2", "pptx2json": "^0.0.10", "process": "^0.11.10", "qrcode-terminal": "^0.12.0", - "qrcode.react": "^4.2.0", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "react-i18next": "^14.0.5", - "react-markdown": "^10.1.0", - "react-router-dom": "^7.8.0", - "react-syntax-highlighter": "^16.1.0", - "react-virtuoso": "^4.18.1", - "rehype-katex": "^7.0.1", - "rehype-raw": "^7.0.0", - "remark-breaks": "^4.0.0", - "remark-gfm": "^4.0.1", - "remark-math": "^6.0.0", "semver": "^7.7.2", "sharp": "^0.34.3", "smol-toml": "^1.6.1", "stream-browserify": "^3.0.0", - "streamdown": "^1.5.1", "strip-json-comments": "^3.1.1", - "swr": "^2.3.6", "tiny-csrf": "^1.1.6", "turndown": "^7.2.2", "turndown-plugin-gfm": "^1.0.2", @@ -100,8 +63,21 @@ "zod": "^3.25.76", }, "devDependencies": { + "@arco-design/web-react": "^2.66.1", + "@codemirror/commands": "^6.8.1", + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-html": "^6.4.11", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/view": "^6.38.6", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@electron/fuses": "^1.8.0", "@electron/notarize": "^3.1.0", + "@floating-ui/react": "^0.27.16", + "@icon-park/react": "^1.4.2", + "@monaco-editor/react": "^4.7.0", "@playwright/test": "^1.58.2", "@sentry/vite-plugin": "^5.1.1", "@testing-library/jest-dom": "^6.9.1", @@ -123,24 +99,48 @@ "@types/turndown": "^5.0.6", "@types/ws": "^8.18.1", "@types/yauzl": "^2.10.3", + "@uiw/codemirror-extensions-langs": "^4.25.1", + "@uiw/react-codemirror": "^4.25.2", "@vitest/coverage-v8": "^4.0.18", "builder-util": "^26.8.1", + "classnames": "^2.5.1", "cross-env": "^7.0.3", + "dayjs": "^1.11.18", + "diff": "^8.0.3", + "diff2html": "^3.4.55", "dotenv": "^17.2.1", "electron": "^37.10.3", "electron-builder": "^26.6.0", "electron-devtools-installer": "^4.0.0", "electron-vite": "^5.0.0", "esbuild": "^0.25.11", + "eventemitter3": "^5.0.1", "husky": "^9.1.7", "jest": "^30.1.3", "jest-diff": "^30.0.4", "jsdom": "^28.1.0", + "katex": "^0.16.22", "lint-staged": "^16.2.7", + "mermaid": "^11.13.0", "oxfmt": "^0.41.0", "oxlint": "^1.56.0", "patch-package": "^8.0.0", "playwright": "^1.59.1", + "qrcode.react": "^4.2.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-i18next": "^14.0.5", + "react-markdown": "^10.1.0", + "react-router-dom": "^7.8.0", + "react-syntax-highlighter": "^16.1.0", + "react-virtuoso": "^4.18.1", + "rehype-katex": "^7.0.1", + "rehype-raw": "^7.0.0", + "remark-breaks": "^4.0.0", + "remark-gfm": "^4.0.1", + "remark-math": "^6.0.0", + "streamdown": "^1.5.1", + "swr": "^2.3.6", "ts-morph": "^28.0.0", "ts-node": "^10.9.2", "tsx": "^4.19.1", diff --git a/docs/evaos/workbench-renderer-dependency-ledger.md b/docs/evaos/workbench-renderer-dependency-ledger.md new file mode 100644 index 00000000000..5207cc50804 --- /dev/null +++ b/docs/evaos/workbench-renderer-dependency-ledger.md @@ -0,0 +1,58 @@ +# Workbench Renderer Dependency Ledger + +This ledger supports #452. Its purpose is to keep renderer-only libraries out of production raw `app.asar/node_modules` while preserving the runtime packages used by the main process, preload scripts, native modules, MCP servers, packaged resources, and updater/release flows. + +## Rule + +Move a root package from `dependencies` to `devDependencies` only when all of these are true: + +- It is imported only by `packages/desktop/src/renderer/**` or renderer-focused tests. +- It is bundled by the renderer Vite build into `out/renderer`. +- It is not imported by `packages/desktop/src/process/**`, `packages/desktop/src/preload/**`, `packages/desktop/src/common/**`, `packages/web-host/**`, `packages/web-cli/**`, or `scripts/**`. +- It is not explicitly included in `packages/desktop/electron-builder.yml` as a raw runtime `node_modules` or `asarUnpack` resource. + +## Moved Renderer-Only Packages + +| Package | Renderer evidence | Runtime evidence | Notes | +| ------------------------------------------------------------------------------------------ | ------------------------------------------------ | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `@arco-design/web-react` | 204 renderer imports plus DOM tests | no process/preload/common/script imports | UI component library; bundled into renderer. | +| `@icon-park/react` | 122 renderer imports | no process/preload/common/script imports | Icon library; bundled into renderer. | +| `mermaid` | `components/Markdown/MermaidBlock.tsx` | no process/preload/common/script imports | Large diagram renderer; bundled into renderer. | +| `react-syntax-highlighter` | Markdown, diff, and code preview renderers | no process/preload/common/script imports | Code highlighting stays in renderer chunks. | +| `@monaco-editor/react` | HTML preview viewer | no process/preload/common/script imports | Editor surface stays renderer-bundled. | +| `@uiw/react-codemirror`, `@uiw/codemirror-extensions-langs`, `@codemirror/*` | settings editors and preview editors | no process/preload/common/script imports | CodeMirror editor dependencies stay build-time/renderer only. | +| `diff`, `diff2html` | diff viewer and renderer message diff UI | no process/preload/common/script imports | Diff UI stays renderer-bundled. | +| `katex`, `react-markdown`, `rehype-*`, `remark-*`, `streamdown` | Markdown and preview rendering | no process/preload/common/script imports | Markdown/math rendering stays renderer-bundled. | +| `react`, `react-dom`, `react-i18next`, `react-router-dom`, `react-virtuoso`, `swr` | renderer app, router, list, i18n, and data hooks | no process/preload/common/script imports | Runtime UI libraries are bundled into `out/renderer`; the app does not require raw React from `app.asar/node_modules`. | +| `@dnd-kit/*`, `@floating-ui/react`, `classnames`, `dayjs`, `eventemitter3`, `qrcode.react` | renderer UI helpers | no process/preload/common/script imports | Small direct dependencies kept out of production raw shipping when used only by renderer. | + +## Kept Or Deferred Packages + +These package families stay in `dependencies` for this PR. Some are proven runtime keepers; others are deferred because #452 is intentionally limited to renderer-only direct dependency moves, not broad unused-dependency removal. + +- Proven main/process/preload/updater keepers: `electron-log`, `electron-squirrel-startup`, `electron-updater`, `fix-path`, `i18next`, `semver`, `zod`. +- Proven native or explicit package-shape keepers: `better-sqlite3`, `web-tree-sitter`, plus the explicit `electron-builder.yml` `node_modules` and `asarUnpack` allowlist. +- Proven API, bridge, agent, or preload keepers: `@anthropic-ai/sdk`, `@google/genai`, `@modelcontextprotocol/sdk`, `@office-ai/aioncli-core`, `@office-ai/platform`, `@sentry/electron`, `openai`. +- Deferred latent or ambiguous common/runtime packages: `docx`, `html-to-text`, `jsonrepair`, `jsonwebtoken`, `mammoth`, `officeparser`, `pptx2json`, `qrcode-terminal`, `smol-toml`, `strip-json-comments`, `turndown`, `turndown-plugin-gfm`, `xlsx-republish`, `@xmldom/xmldom`. +- Deferred unused-looking server/channel/runtime packages that need a separate removal issue and package proof: `@agentclientprotocol/sdk`, `@aws-sdk/client-bedrock`, `@grammyjs/transformer-throttler`, `@larksuiteoapi/node-sdk`, `@wecom/aibot-node-sdk`, `bcryptjs`, `cookie`, `cookie-parser`, `cors`, `dingtalk-stream`, `express`, `express-rate-limit`, `grammy`, `multer`, `sharp`, `tiny-csrf`, `ws`, `yauzl`. +- Browser/node polyfill packages that need a separate audit before removal: `buffer`, `process`, `stream-browserify`. + +## Verification + +Local source checks: + +```bash +rg -n "from ['\"](@arco-design/web-react|@icon-park/react|mermaid|react-syntax-highlighter|@monaco-editor/react|@uiw/react-codemirror|diff2html|react-markdown|streamdown|swr)" \ + packages/desktop/src/common packages/desktop/src/process packages/desktop/src/preload packages/web-host/src packages/web-cli/src scripts +``` + +Remote package proof should inspect the packaged app after `electron-builder --dir` or a thin/functional smoke artifact: + +```bash +npx asar list "out/mac-arm64/AionUi.app/Contents/Resources/app.asar" \ + | grep -E '^/node_modules/(@arco-design|@icon-park|mermaid|react-syntax-highlighter|@monaco-editor|@uiw|react-markdown|streamdown|diff2html)(/|$)' && exit 1 || true +``` + +Also inspect `app.asar.unpacked/node_modules`. Absence from `app.asar` alone is not enough because native/WASM/runtime packages are intentionally unpacked. + +The renderer bundle must still launch and cover Markdown, diff UI, Mermaid diagrams, code highlighting, editor surfaces, settings, and updater UI. diff --git a/package.json b/package.json index ed1ce3ebfd0..b72cbf94669 100644 --- a/package.json +++ b/package.json @@ -72,50 +72,30 @@ "@agentclientprotocol/sdk": "^0.18.2", "@aionui/web-host": "workspace:*", "@anthropic-ai/sdk": "^0.71.2", - "@arco-design/web-react": "^2.66.1", "@aws-sdk/client-bedrock": "^3.987.0", - "@codemirror/commands": "^6.8.1", - "@codemirror/lang-css": "^6.3.1", - "@codemirror/lang-html": "^6.4.11", - "@codemirror/lang-json": "^6.0.2", - "@codemirror/lang-markdown": "^6.5.0", - "@codemirror/view": "^6.38.6", - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", - "@floating-ui/react": "^0.27.16", "@google/genai": "^1.16.0", "@grammyjs/transformer-throttler": "^1.2.1", - "@icon-park/react": "^1.4.2", "@larksuiteoapi/node-sdk": "^1.58.0", "@modelcontextprotocol/sdk": "^1.20.0", - "@monaco-editor/react": "^4.7.0", "@office-ai/aioncli-core": "^0.30.6", "@office-ai/platform": "^0.3.16", "@sentry/electron": "^7.10.0", "@types/bcryptjs": "^2.4.6", "@types/jsonwebtoken": "^9.0.10", - "@uiw/codemirror-extensions-langs": "^4.25.1", - "@uiw/react-codemirror": "^4.25.2", "@wecom/aibot-node-sdk": "^1.0.6", "@xmldom/xmldom": "^0.8.11", "bcryptjs": "^2.4.3", "better-sqlite3": "^12.4.1", "buffer": "^6.0.3", - "classnames": "^2.5.1", "cookie": "^1.0.2", "cookie-parser": "^1.4.7", "cors": "^2.8.5", "croner": "^9.1.0", - "dayjs": "^1.11.18", - "diff": "^8.0.3", - "diff2html": "^3.4.55", "dingtalk-stream": "^2.1.4", "docx": "^9.5.1", "electron-log": "^5.4.3", "electron-squirrel-startup": "^1.0.1", "electron-updater": "^6.6.2", - "eventemitter3": "^5.0.1", "express": "^5.1.0", "express-rate-limit": "^7.5.1", "fix-path": "^4.0.0", @@ -125,35 +105,18 @@ "i18next": "^23.7.16", "jsonrepair": "^3.13.0", "jsonwebtoken": "^9.0.2", - "katex": "^0.16.22", "mammoth": "^1.11.0", - "mermaid": "^11.13.0", "multer": "^2.1.1", "officeparser": "^5.2.2", "openai": "^5.12.2", "pptx2json": "^0.0.10", "process": "^0.11.10", "qrcode-terminal": "^0.12.0", - "qrcode.react": "^4.2.0", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "react-i18next": "^14.0.5", - "react-markdown": "^10.1.0", - "react-router-dom": "^7.8.0", - "react-syntax-highlighter": "^16.1.0", - "react-virtuoso": "^4.18.1", - "rehype-katex": "^7.0.1", - "rehype-raw": "^7.0.0", - "remark-breaks": "^4.0.0", - "remark-gfm": "^4.0.1", - "remark-math": "^6.0.0", "semver": "^7.7.2", "sharp": "^0.34.3", "smol-toml": "^1.6.1", "stream-browserify": "^3.0.0", - "streamdown": "^1.5.1", "strip-json-comments": "^3.1.1", - "swr": "^2.3.6", "tiny-csrf": "^1.1.6", "turndown": "^7.2.2", "turndown-plugin-gfm": "^1.0.2", @@ -164,8 +127,21 @@ "zod": "^3.25.76" }, "devDependencies": { + "@arco-design/web-react": "^2.66.1", + "@codemirror/commands": "^6.8.1", + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-html": "^6.4.11", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/view": "^6.38.6", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@electron/fuses": "^1.8.0", "@electron/notarize": "^3.1.0", + "@floating-ui/react": "^0.27.16", + "@icon-park/react": "^1.4.2", + "@monaco-editor/react": "^4.7.0", "@playwright/test": "^1.58.2", "@sentry/vite-plugin": "^5.1.1", "@testing-library/jest-dom": "^6.9.1", @@ -187,24 +163,48 @@ "@types/turndown": "^5.0.6", "@types/ws": "^8.18.1", "@types/yauzl": "^2.10.3", + "@uiw/codemirror-extensions-langs": "^4.25.1", + "@uiw/react-codemirror": "^4.25.2", "@vitest/coverage-v8": "^4.0.18", "builder-util": "^26.8.1", + "classnames": "^2.5.1", "cross-env": "^7.0.3", + "dayjs": "^1.11.18", + "diff": "^8.0.3", + "diff2html": "^3.4.55", "dotenv": "^17.2.1", "electron": "^37.10.3", "electron-builder": "^26.6.0", "electron-devtools-installer": "^4.0.0", "electron-vite": "^5.0.0", "esbuild": "^0.25.11", + "eventemitter3": "^5.0.1", "husky": "^9.1.7", "jest": "^30.1.3", "jest-diff": "^30.0.4", "jsdom": "^28.1.0", + "katex": "^0.16.22", "lint-staged": "^16.2.7", + "mermaid": "^11.13.0", "oxfmt": "^0.41.0", "oxlint": "^1.56.0", "patch-package": "^8.0.0", "playwright": "^1.59.1", + "qrcode.react": "^4.2.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-i18next": "^14.0.5", + "react-markdown": "^10.1.0", + "react-router-dom": "^7.8.0", + "react-syntax-highlighter": "^16.1.0", + "react-virtuoso": "^4.18.1", + "rehype-katex": "^7.0.1", + "rehype-raw": "^7.0.0", + "remark-breaks": "^4.0.0", + "remark-gfm": "^4.0.1", + "remark-math": "^6.0.0", + "streamdown": "^1.5.1", + "swr": "^2.3.6", "ts-morph": "^28.0.0", "ts-node": "^10.9.2", "tsx": "^4.19.1", From ffdaf0659d8732de28082b8f87ca18e60f945fab Mon Sep 17 00:00:00 2001 From: Eva Date: Sat, 27 Jun 2026 18:33:37 +0700 Subject: [PATCH 2/7] Verify renderer dependency pruning in thin smoke --- .github/workflows/pr-checks.yml | 2 + .../workbench-renderer-dependency-ledger.md | 7 +- scripts/evaosVerifyRendererDependencyPrune.js | 139 ++++++++++++++++++ ...evaosVerifyRendererDependencyPrune.test.ts | 102 +++++++++++++ 4 files changed, 246 insertions(+), 4 deletions(-) create mode 100644 scripts/evaosVerifyRendererDependencyPrune.js create mode 100644 tests/unit/process/evaosVerifyRendererDependencyPrune.test.ts diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 79df063d7df..0e39c72d8dc 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -707,6 +707,7 @@ jobs: BUNDLE_ID="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP_PATH/Contents/Info.plist")" VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$APP_PATH/Contents/Info.plist")" + node scripts/evaosVerifyRendererDependencyPrune.js "$APP_PATH" { echo "## Thin app smoke" @@ -715,6 +716,7 @@ jobs: echo "- Version: \`$VERSION\`" echo "- Bundle ID: \`$BUNDLE_ID\`" echo "- Package smoke reasons: \`$PACKAGE_SMOKE_REASONS\`" + echo "- Renderer dependency prune: verified \`app.asar\` and \`app.asar.unpacked/node_modules\`" echo echo "This PR artifact is intentionally unpacked and may be ad-hoc/non-Developer-ID signed by packaging hooks. It produces no DMG, no release metadata, no notarization/stapling proof, no TCC or permission proof, and no public release proof." echo diff --git a/docs/evaos/workbench-renderer-dependency-ledger.md b/docs/evaos/workbench-renderer-dependency-ledger.md index 5207cc50804..b63b243ccdc 100644 --- a/docs/evaos/workbench-renderer-dependency-ledger.md +++ b/docs/evaos/workbench-renderer-dependency-ledger.md @@ -46,13 +46,12 @@ rg -n "from ['\"](@arco-design/web-react|@icon-park/react|mermaid|react-syntax-h packages/desktop/src/common packages/desktop/src/process packages/desktop/src/preload packages/web-host/src packages/web-cli/src scripts ``` -Remote package proof should inspect the packaged app after `electron-builder --dir` or a thin/functional smoke artifact: +Remote package proof is enforced by Thin App Smoke after `electron-builder --dir` produces an unpacked `.app`: ```bash -npx asar list "out/mac-arm64/AionUi.app/Contents/Resources/app.asar" \ - | grep -E '^/node_modules/(@arco-design|@icon-park|mermaid|react-syntax-highlighter|@monaco-editor|@uiw|react-markdown|streamdown|diff2html)(/|$)' && exit 1 || true +node scripts/evaosVerifyRendererDependencyPrune.js "out/mac-arm64/AionUi.app" ``` -Also inspect `app.asar.unpacked/node_modules`. Absence from `app.asar` alone is not enough because native/WASM/runtime packages are intentionally unpacked. +The verifier inspects both `app.asar` and `app.asar.unpacked/node_modules`. Absence from `app.asar` alone is not enough because native/WASM/runtime packages are intentionally unpacked. The renderer bundle must still launch and cover Markdown, diff UI, Mermaid diagrams, code highlighting, editor surfaces, settings, and updater UI. diff --git a/scripts/evaosVerifyRendererDependencyPrune.js b/scripts/evaosVerifyRendererDependencyPrune.js new file mode 100644 index 00000000000..6d3a165612a --- /dev/null +++ b/scripts/evaosVerifyRendererDependencyPrune.js @@ -0,0 +1,139 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +const rendererOnlyPackages = [ + '@arco-design/web-react', + '@codemirror/commands', + '@codemirror/lang-css', + '@codemirror/lang-html', + '@codemirror/lang-json', + '@codemirror/lang-markdown', + '@codemirror/view', + '@dnd-kit/core', + '@dnd-kit/sortable', + '@dnd-kit/utilities', + '@floating-ui/react', + '@icon-park/react', + '@monaco-editor/react', + '@uiw/codemirror-extensions-langs', + '@uiw/react-codemirror', + 'classnames', + 'dayjs', + 'diff', + 'diff2html', + 'eventemitter3', + 'katex', + 'mermaid', + 'qrcode.react', + 'react', + 'react-dom', + 'react-i18next', + 'react-markdown', + 'react-router-dom', + 'react-syntax-highlighter', + 'react-virtuoso', + 'rehype-katex', + 'rehype-raw', + 'remark-breaks', + 'remark-gfm', + 'remark-math', + 'streamdown', + 'swr', +]; + +function usage() { + console.error('Usage: node scripts/evaosVerifyRendererDependencyPrune.js '); +} + +function readAsarHeader(archivePath) { + const fd = fs.openSync(archivePath, 'r'); + try { + const sizePickle = Buffer.alloc(8); + fs.readSync(fd, sizePickle, 0, sizePickle.length, 0); + const headerSize = sizePickle.readUInt32LE(4); + const headerPickle = Buffer.alloc(headerSize); + fs.readSync(fd, headerPickle, 0, headerPickle.length, 8); + const jsonSize = headerPickle.readInt32LE(4); + const json = headerPickle.slice(8, 8 + jsonSize).toString('utf8'); + return JSON.parse(json); + } finally { + fs.closeSync(fd); + } +} + +function collectPaths(files, prefix = '') { + const paths = []; + for (const [name, entry] of Object.entries(files || {})) { + const entryPath = `${prefix}/${name}`; + paths.push(entryPath); + if (entry && entry.files) { + paths.push(...collectPaths(entry.files, entryPath)); + } + } + return paths; +} + +function packagePathExists(root, packageName) { + return fs.existsSync(path.join(root, ...packageName.split('/'))); +} + +function verifyRendererDependencyPrune(appPath) { + if (!appPath) { + throw new Error('Missing path to unpacked app'); + } + + const appAsar = path.join(appPath, 'Contents', 'Resources', 'app.asar'); + const unpackedNodeModules = path.join(appPath, 'Contents', 'Resources', 'app.asar.unpacked', 'node_modules'); + + if (!fs.existsSync(appAsar)) { + throw new Error(`Missing packaged app.asar: ${appAsar}`); + } + + const header = readAsarHeader(appAsar); + const asarPaths = collectPaths(header.files); + const leakedInAsar = rendererOnlyPackages.filter((packageName) => { + const root = `/node_modules/${packageName}`; + return asarPaths.some((entryPath) => entryPath === root || entryPath.startsWith(`${root}/`)); + }); + + const leakedInUnpacked = rendererOnlyPackages.filter((packageName) => + packagePathExists(unpackedNodeModules, packageName) + ); + + if (leakedInAsar.length || leakedInUnpacked.length) { + const errors = []; + if (leakedInAsar.length) { + errors.push(`Renderer-only packages leaked into app.asar: ${leakedInAsar.join(', ')}`); + } + if (leakedInUnpacked.length) { + errors.push(`Renderer-only packages leaked into app.asar.unpacked/node_modules: ${leakedInUnpacked.join(', ')}`); + } + throw new Error(errors.join('\n')); + } + + return { checkedPackages: rendererOnlyPackages.length }; +} + +function main() { + try { + const result = verifyRendererDependencyPrune(process.argv[2]); + console.log(`Renderer dependency prune verified for ${result.checkedPackages} packages.`); + } catch (error) { + if (!process.argv[2]) { + usage(); + } + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + } +} + +if (require.main === module) { + main(); +} + +module.exports = { + rendererOnlyPackages, + verifyRendererDependencyPrune, +}; diff --git a/tests/unit/process/evaosVerifyRendererDependencyPrune.test.ts b/tests/unit/process/evaosVerifyRendererDependencyPrune.test.ts new file mode 100644 index 00000000000..6af024a3da0 --- /dev/null +++ b/tests/unit/process/evaosVerifyRendererDependencyPrune.test.ts @@ -0,0 +1,102 @@ +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { createRequire } from 'node:module'; +import { afterEach, describe, expect, it } from 'vitest'; + +const require = createRequire(import.meta.url); +const verifier = require('../../../scripts/evaosVerifyRendererDependencyPrune.js') as { + rendererOnlyPackages: string[]; + verifyRendererDependencyPrune: (appPath: string) => { checkedPackages: number }; +}; + +let tempDir: string | undefined; + +function align4(value: number) { + return value + ((4 - (value % 4)) % 4); +} + +function addPackage(files: Record, packageName: string) { + let current = files; + for (const part of packageName.split('/')) { + const entry = (current[part] ??= { files: {} }) as { files: Record }; + current = entry.files; + } +} + +function writeFakeAsar(appPath: string, packages: string[]) { + const resources = path.join(appPath, 'Contents', 'Resources'); + fs.mkdirSync(resources, { recursive: true }); + + const nodeModules = { files: {} as Record }; + for (const packageName of packages) { + addPackage(nodeModules.files, packageName); + } + + const header = { + files: packages.length ? { node_modules: nodeModules } : { renderer: { files: {} } }, + }; + const json = Buffer.from(JSON.stringify(header), 'utf8'); + const headerPayloadSize = 4 + align4(json.length); + const headerPickle = Buffer.alloc(4 + headerPayloadSize); + headerPickle.writeUInt32LE(headerPayloadSize, 0); + headerPickle.writeInt32LE(json.length, 4); + json.copy(headerPickle, 8); + + const sizePickle = Buffer.alloc(8); + sizePickle.writeUInt32LE(4, 0); + sizePickle.writeUInt32LE(headerPickle.length, 4); + + fs.writeFileSync(path.join(resources, 'app.asar'), Buffer.concat([sizePickle, headerPickle])); +} + +function makeApp(packages: string[] = []) { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evaos-renderer-prune-')); + const appPath = path.join(tempDir, 'AionUi.app'); + writeFakeAsar(appPath, packages); + return appPath; +} + +describe('evaosVerifyRendererDependencyPrune', () => { + afterEach(() => { + if (tempDir) { + fs.rmSync(tempDir, { recursive: true, force: true }); + tempDir = undefined; + } + }); + + it('passes when renderer-only packages are absent from raw package locations', () => { + const appPath = makeApp(); + + expect(verifier.verifyRendererDependencyPrune(appPath)).toEqual({ + checkedPackages: verifier.rendererOnlyPackages.length, + }); + }); + + it('fails if any moved renderer dependency ships inside app.asar node_modules', () => { + const appPath = makeApp(verifier.rendererOnlyPackages); + + expect(() => verifier.verifyRendererDependencyPrune(appPath)).toThrow(verifier.rendererOnlyPackages[0]); + + try { + verifier.verifyRendererDependencyPrune(appPath); + throw new Error('Expected verifier to fail'); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + for (const packageName of verifier.rendererOnlyPackages) { + expect(message).toContain(packageName); + } + } + }); + + it('fails if a moved renderer dependency ships inside app.asar.unpacked node_modules', () => { + const appPath = makeApp(); + const leakedPackage = 'react-router-dom'; + fs.mkdirSync( + path.join(appPath, 'Contents', 'Resources', 'app.asar.unpacked', 'node_modules', ...leakedPackage.split('/')), + { recursive: true } + ); + + expect(() => verifier.verifyRendererDependencyPrune(appPath)).toThrow(leakedPackage); + }); +}); From 4a9579389a6e1548203df459ac04ecc1c8df5218 Mon Sep 17 00:00:00 2001 From: Eva Date: Sat, 27 Jun 2026 18:41:05 +0700 Subject: [PATCH 3/7] Keep runtime transitive deps in production manifest --- bun.lock | 4 +-- .../workbench-renderer-dependency-ledger.md | 27 ++++++++++--------- package.json | 4 +-- scripts/evaosVerifyRendererDependencyPrune.js | 2 -- 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/bun.lock b/bun.lock index 1fb5abe9517..8c755f121a1 100644 --- a/bun.lock +++ b/bun.lock @@ -27,11 +27,13 @@ "cookie-parser": "^1.4.7", "cors": "^2.8.5", "croner": "^9.1.0", + "diff": "^8.0.3", "dingtalk-stream": "^2.1.4", "docx": "^9.5.1", "electron-log": "^5.4.3", "electron-squirrel-startup": "^1.0.1", "electron-updater": "^6.6.2", + "eventemitter3": "^5.0.1", "express": "^5.1.0", "express-rate-limit": "^7.5.1", "fix-path": "^4.0.0", @@ -106,7 +108,6 @@ "classnames": "^2.5.1", "cross-env": "^7.0.3", "dayjs": "^1.11.18", - "diff": "^8.0.3", "diff2html": "^3.4.55", "dotenv": "^17.2.1", "electron": "^37.10.3", @@ -114,7 +115,6 @@ "electron-devtools-installer": "^4.0.0", "electron-vite": "^5.0.0", "esbuild": "^0.25.11", - "eventemitter3": "^5.0.1", "husky": "^9.1.7", "jest": "^30.1.3", "jest-diff": "^30.0.4", diff --git a/docs/evaos/workbench-renderer-dependency-ledger.md b/docs/evaos/workbench-renderer-dependency-ledger.md index b63b243ccdc..e0882fae5fc 100644 --- a/docs/evaos/workbench-renderer-dependency-ledger.md +++ b/docs/evaos/workbench-renderer-dependency-ledger.md @@ -13,18 +13,18 @@ Move a root package from `dependencies` to `devDependencies` only when all of th ## Moved Renderer-Only Packages -| Package | Renderer evidence | Runtime evidence | Notes | -| ------------------------------------------------------------------------------------------ | ------------------------------------------------ | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -| `@arco-design/web-react` | 204 renderer imports plus DOM tests | no process/preload/common/script imports | UI component library; bundled into renderer. | -| `@icon-park/react` | 122 renderer imports | no process/preload/common/script imports | Icon library; bundled into renderer. | -| `mermaid` | `components/Markdown/MermaidBlock.tsx` | no process/preload/common/script imports | Large diagram renderer; bundled into renderer. | -| `react-syntax-highlighter` | Markdown, diff, and code preview renderers | no process/preload/common/script imports | Code highlighting stays in renderer chunks. | -| `@monaco-editor/react` | HTML preview viewer | no process/preload/common/script imports | Editor surface stays renderer-bundled. | -| `@uiw/react-codemirror`, `@uiw/codemirror-extensions-langs`, `@codemirror/*` | settings editors and preview editors | no process/preload/common/script imports | CodeMirror editor dependencies stay build-time/renderer only. | -| `diff`, `diff2html` | diff viewer and renderer message diff UI | no process/preload/common/script imports | Diff UI stays renderer-bundled. | -| `katex`, `react-markdown`, `rehype-*`, `remark-*`, `streamdown` | Markdown and preview rendering | no process/preload/common/script imports | Markdown/math rendering stays renderer-bundled. | -| `react`, `react-dom`, `react-i18next`, `react-router-dom`, `react-virtuoso`, `swr` | renderer app, router, list, i18n, and data hooks | no process/preload/common/script imports | Runtime UI libraries are bundled into `out/renderer`; the app does not require raw React from `app.asar/node_modules`. | -| `@dnd-kit/*`, `@floating-ui/react`, `classnames`, `dayjs`, `eventemitter3`, `qrcode.react` | renderer UI helpers | no process/preload/common/script imports | Small direct dependencies kept out of production raw shipping when used only by renderer. | +| Package | Renderer evidence | Runtime evidence | Notes | +| ---------------------------------------------------------------------------------- | ------------------------------------------------ | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `@arco-design/web-react` | 204 renderer imports plus DOM tests | no process/preload/common/script imports | UI component library; bundled into renderer. | +| `@icon-park/react` | 122 renderer imports | no process/preload/common/script imports | Icon library; bundled into renderer. | +| `mermaid` | `components/Markdown/MermaidBlock.tsx` | no process/preload/common/script imports | Large diagram renderer; bundled into renderer. | +| `react-syntax-highlighter` | Markdown, diff, and code preview renderers | no process/preload/common/script imports | Code highlighting stays in renderer chunks. | +| `@monaco-editor/react` | HTML preview viewer | no process/preload/common/script imports | Editor surface stays renderer-bundled. | +| `@uiw/react-codemirror`, `@uiw/codemirror-extensions-langs`, `@codemirror/*` | settings editors and preview editors | no process/preload/common/script imports | CodeMirror editor dependencies stay build-time/renderer only. | +| `diff2html` | diff viewer UI | no process/preload/common/script imports | Diff UI wrapper stays renderer-bundled. | +| `katex`, `react-markdown`, `rehype-*`, `remark-*`, `streamdown` | Markdown and preview rendering | no process/preload/common/script imports | Markdown/math rendering stays renderer-bundled. | +| `react`, `react-dom`, `react-i18next`, `react-router-dom`, `react-virtuoso`, `swr` | renderer app, router, list, i18n, and data hooks | no process/preload/common/script imports | Runtime UI libraries are bundled into `out/renderer`; the app does not require raw React from `app.asar/node_modules`. | +| `@dnd-kit/*`, `@floating-ui/react`, `classnames`, `dayjs`, `qrcode.react` | renderer UI helpers | no process/preload/common/script imports | Small direct dependencies kept out of production raw shipping when used only by renderer. | ## Kept Or Deferred Packages @@ -33,6 +33,7 @@ These package families stay in `dependencies` for this PR. Some are proven runti - Proven main/process/preload/updater keepers: `electron-log`, `electron-squirrel-startup`, `electron-updater`, `fix-path`, `i18next`, `semver`, `zod`. - Proven native or explicit package-shape keepers: `better-sqlite3`, `web-tree-sitter`, plus the explicit `electron-builder.yml` `node_modules` and `asarUnpack` allowlist. - Proven API, bridge, agent, or preload keepers: `@anthropic-ai/sdk`, `@google/genai`, `@modelcontextprotocol/sdk`, `@office-ai/aioncli-core`, `@office-ai/platform`, `@sentry/electron`, `openai`. +- Proven runtime-transitive keepers found by Thin App Smoke: `diff` is required by `@office-ai/aioncli-core`; `eventemitter3` is required by `@office-ai/platform` and `@wecom/aibot-node-sdk`. - Deferred latent or ambiguous common/runtime packages: `docx`, `html-to-text`, `jsonrepair`, `jsonwebtoken`, `mammoth`, `officeparser`, `pptx2json`, `qrcode-terminal`, `smol-toml`, `strip-json-comments`, `turndown`, `turndown-plugin-gfm`, `xlsx-republish`, `@xmldom/xmldom`. - Deferred unused-looking server/channel/runtime packages that need a separate removal issue and package proof: `@agentclientprotocol/sdk`, `@aws-sdk/client-bedrock`, `@grammyjs/transformer-throttler`, `@larksuiteoapi/node-sdk`, `@wecom/aibot-node-sdk`, `bcryptjs`, `cookie`, `cookie-parser`, `cors`, `dingtalk-stream`, `express`, `express-rate-limit`, `grammy`, `multer`, `sharp`, `tiny-csrf`, `ws`, `yauzl`. - Browser/node polyfill packages that need a separate audit before removal: `buffer`, `process`, `stream-browserify`. @@ -42,7 +43,7 @@ These package families stay in `dependencies` for this PR. Some are proven runti Local source checks: ```bash -rg -n "from ['\"](@arco-design/web-react|@icon-park/react|mermaid|react-syntax-highlighter|@monaco-editor/react|@uiw/react-codemirror|diff2html|react-markdown|streamdown|swr)" \ +rg -n "from ['\"](@arco-design/web-react|@codemirror/(commands|lang-css|lang-html|lang-json|lang-markdown|view)|@dnd-kit/(core|sortable|utilities)|@floating-ui/react|@icon-park/react|@monaco-editor/react|@uiw/(codemirror-extensions-langs|react-codemirror)|classnames|dayjs|diff2html|katex|mermaid|qrcode\\.react|react|react-dom|react-i18next|react-markdown|react-router-dom|react-syntax-highlighter|react-virtuoso|rehype-(katex|raw)|remark-(breaks|gfm|math)|streamdown|swr)(/[^'\"]*)?['\"]" \ packages/desktop/src/common packages/desktop/src/process packages/desktop/src/preload packages/web-host/src packages/web-cli/src scripts ``` diff --git a/package.json b/package.json index b72cbf94669..6d90ff31a36 100644 --- a/package.json +++ b/package.json @@ -91,11 +91,13 @@ "cookie-parser": "^1.4.7", "cors": "^2.8.5", "croner": "^9.1.0", + "diff": "^8.0.3", "dingtalk-stream": "^2.1.4", "docx": "^9.5.1", "electron-log": "^5.4.3", "electron-squirrel-startup": "^1.0.1", "electron-updater": "^6.6.2", + "eventemitter3": "^5.0.1", "express": "^5.1.0", "express-rate-limit": "^7.5.1", "fix-path": "^4.0.0", @@ -170,7 +172,6 @@ "classnames": "^2.5.1", "cross-env": "^7.0.3", "dayjs": "^1.11.18", - "diff": "^8.0.3", "diff2html": "^3.4.55", "dotenv": "^17.2.1", "electron": "^37.10.3", @@ -178,7 +179,6 @@ "electron-devtools-installer": "^4.0.0", "electron-vite": "^5.0.0", "esbuild": "^0.25.11", - "eventemitter3": "^5.0.1", "husky": "^9.1.7", "jest": "^30.1.3", "jest-diff": "^30.0.4", diff --git a/scripts/evaosVerifyRendererDependencyPrune.js b/scripts/evaosVerifyRendererDependencyPrune.js index 6d3a165612a..9d0f96c6bac 100644 --- a/scripts/evaosVerifyRendererDependencyPrune.js +++ b/scripts/evaosVerifyRendererDependencyPrune.js @@ -21,9 +21,7 @@ const rendererOnlyPackages = [ '@uiw/react-codemirror', 'classnames', 'dayjs', - 'diff', 'diff2html', - 'eventemitter3', 'katex', 'mermaid', 'qrcode.react', From e76f65b084fd25170a6c7bb7d4dd1f4a1eba2659 Mon Sep 17 00:00:00 2001 From: Eva Date: Sat, 27 Jun 2026 18:44:07 +0700 Subject: [PATCH 4/7] Guard renderer prune package list --- .github/workflows/pr-checks.yml | 1 + .../workbench-renderer-dependency-ledger.md | 3 +- scripts/evaosVerifyRendererDependencyPrune.js | 61 ++++++++++++++++++- ...evaosVerifyRendererDependencyPrune.test.ts | 12 ++++ 4 files changed, 75 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 0e39c72d8dc..6d25b8c79b5 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -707,6 +707,7 @@ jobs: BUNDLE_ID="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP_PATH/Contents/Info.plist")" VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$APP_PATH/Contents/Info.plist")" + node scripts/evaosVerifyRendererDependencyPrune.js --verify-package-json node scripts/evaosVerifyRendererDependencyPrune.js "$APP_PATH" { diff --git a/docs/evaos/workbench-renderer-dependency-ledger.md b/docs/evaos/workbench-renderer-dependency-ledger.md index e0882fae5fc..09b08aa368d 100644 --- a/docs/evaos/workbench-renderer-dependency-ledger.md +++ b/docs/evaos/workbench-renderer-dependency-ledger.md @@ -50,9 +50,10 @@ rg -n "from ['\"](@arco-design/web-react|@codemirror/(commands|lang-css|lang-htm Remote package proof is enforced by Thin App Smoke after `electron-builder --dir` produces an unpacked `.app`: ```bash +node scripts/evaosVerifyRendererDependencyPrune.js --verify-package-json node scripts/evaosVerifyRendererDependencyPrune.js "out/mac-arm64/AionUi.app" ``` -The verifier inspects both `app.asar` and `app.asar.unpacked/node_modules`. Absence from `app.asar` alone is not enough because native/WASM/runtime packages are intentionally unpacked. +The verifier first checks the package-list buckets against `package.json`, then inspects both `app.asar` and `app.asar.unpacked/node_modules`. Absence from `app.asar` alone is not enough because native/WASM/runtime packages are intentionally unpacked. The renderer bundle must still launch and cover Markdown, diff UI, Mermaid diagrams, code highlighting, editor surfaces, settings, and updater UI. diff --git a/scripts/evaosVerifyRendererDependencyPrune.js b/scripts/evaosVerifyRendererDependencyPrune.js index 9d0f96c6bac..8131f707457 100644 --- a/scripts/evaosVerifyRendererDependencyPrune.js +++ b/scripts/evaosVerifyRendererDependencyPrune.js @@ -41,10 +41,20 @@ const rendererOnlyPackages = [ 'swr', ]; +const runtimeTransitivePackages = ['diff', 'eventemitter3']; + function usage() { - console.error('Usage: node scripts/evaosVerifyRendererDependencyPrune.js '); + console.error( + 'Usage: node scripts/evaosVerifyRendererDependencyPrune.js ' + ); } +/** + * Reads and parses the file-tree header from an Electron asar archive. + * + * @param {string} archivePath Path to the `app.asar` archive. + * @returns {{ files?: Record }} Parsed asar header object. + */ function readAsarHeader(archivePath) { const fd = fs.openSync(archivePath, 'r'); try { @@ -77,6 +87,46 @@ function packagePathExists(root, packageName) { return fs.existsSync(path.join(root, ...packageName.split('/'))); } +/** + * Verifies that the dependency-prune package lists match package.json buckets. + * + * @param {string} [packageJsonPath] Path to the package manifest to inspect. + * @returns {{ checkedRendererPackages: number, checkedRuntimeTransitivePackages: number }} Count of checked package names. + */ +function verifyRendererDependencyManifest(packageJsonPath = path.join(__dirname, '..', 'package.json')) { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + const dependencies = packageJson.dependencies || {}; + const devDependencies = packageJson.devDependencies || {}; + const errors = []; + + for (const packageName of rendererOnlyPackages) { + if (dependencies[packageName] || !devDependencies[packageName]) { + errors.push(`${packageName} must be in devDependencies only`); + } + } + + for (const packageName of runtimeTransitivePackages) { + if (!dependencies[packageName] || devDependencies[packageName]) { + errors.push(`${packageName} must stay in dependencies only`); + } + } + + if (errors.length) { + throw new Error(errors.join('\n')); + } + + return { + checkedRendererPackages: rendererOnlyPackages.length, + checkedRuntimeTransitivePackages: runtimeTransitivePackages.length, + }; +} + +/** + * Verifies an unpacked Electron app does not raw-ship renderer-only packages. + * + * @param {string} appPath Path to the unpacked `.app` bundle. + * @returns {{ checkedPackages: number }} Count of renderer-only package names checked. + */ function verifyRendererDependencyPrune(appPath) { if (!appPath) { throw new Error('Missing path to unpacked app'); @@ -116,6 +166,13 @@ function verifyRendererDependencyPrune(appPath) { function main() { try { + if (process.argv[2] === '--verify-package-json') { + const result = verifyRendererDependencyManifest(process.argv[3]); + console.log( + `Renderer dependency manifest verified for ${result.checkedRendererPackages} renderer packages and ${result.checkedRuntimeTransitivePackages} runtime-transitive packages.` + ); + return; + } const result = verifyRendererDependencyPrune(process.argv[2]); console.log(`Renderer dependency prune verified for ${result.checkedPackages} packages.`); } catch (error) { @@ -133,5 +190,7 @@ if (require.main === module) { module.exports = { rendererOnlyPackages, + runtimeTransitivePackages, + verifyRendererDependencyManifest, verifyRendererDependencyPrune, }; diff --git a/tests/unit/process/evaosVerifyRendererDependencyPrune.test.ts b/tests/unit/process/evaosVerifyRendererDependencyPrune.test.ts index 6af024a3da0..55812a93acc 100644 --- a/tests/unit/process/evaosVerifyRendererDependencyPrune.test.ts +++ b/tests/unit/process/evaosVerifyRendererDependencyPrune.test.ts @@ -7,6 +7,11 @@ import { afterEach, describe, expect, it } from 'vitest'; const require = createRequire(import.meta.url); const verifier = require('../../../scripts/evaosVerifyRendererDependencyPrune.js') as { rendererOnlyPackages: string[]; + runtimeTransitivePackages: string[]; + verifyRendererDependencyManifest: (packageJsonPath?: string) => { + checkedRendererPackages: number; + checkedRuntimeTransitivePackages: number; + }; verifyRendererDependencyPrune: (appPath: string) => { checkedPackages: number }; }; @@ -73,6 +78,13 @@ describe('evaosVerifyRendererDependencyPrune', () => { }); }); + it('keeps the verifier package lists in sync with package.json buckets', () => { + expect(verifier.verifyRendererDependencyManifest(require.resolve('../../../package.json'))).toEqual({ + checkedRendererPackages: verifier.rendererOnlyPackages.length, + checkedRuntimeTransitivePackages: verifier.runtimeTransitivePackages.length, + }); + }); + it('fails if any moved renderer dependency ships inside app.asar node_modules', () => { const appPath = makeApp(verifier.rendererOnlyPackages); From d12f31d0993c57eb0b181ab78a95b66fb4440ad8 Mon Sep 17 00:00:00 2001 From: Eva Date: Sat, 27 Jun 2026 18:47:08 +0700 Subject: [PATCH 5/7] Expand renderer dependency import audit --- docs/evaos/workbench-renderer-dependency-ledger.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/evaos/workbench-renderer-dependency-ledger.md b/docs/evaos/workbench-renderer-dependency-ledger.md index 09b08aa368d..282cf5781de 100644 --- a/docs/evaos/workbench-renderer-dependency-ledger.md +++ b/docs/evaos/workbench-renderer-dependency-ledger.md @@ -43,7 +43,8 @@ These package families stay in `dependencies` for this PR. Some are proven runti Local source checks: ```bash -rg -n "from ['\"](@arco-design/web-react|@codemirror/(commands|lang-css|lang-html|lang-json|lang-markdown|view)|@dnd-kit/(core|sortable|utilities)|@floating-ui/react|@icon-park/react|@monaco-editor/react|@uiw/(codemirror-extensions-langs|react-codemirror)|classnames|dayjs|diff2html|katex|mermaid|qrcode\\.react|react|react-dom|react-i18next|react-markdown|react-router-dom|react-syntax-highlighter|react-virtuoso|rehype-(katex|raw)|remark-(breaks|gfm|math)|streamdown|swr)(/[^'\"]*)?['\"]" \ +MOVED_RENDERER_PACKAGE_PATTERN='@arco-design/web-react|@codemirror/(commands|lang-css|lang-html|lang-json|lang-markdown|view)|@dnd-kit/(core|sortable|utilities)|@floating-ui/react|@icon-park/react|@monaco-editor/react|@uiw/(codemirror-extensions-langs|react-codemirror)|classnames|dayjs|diff2html|katex|mermaid|qrcode\.react|react|react-dom|react-i18next|react-markdown|react-router-dom|react-syntax-highlighter|react-virtuoso|rehype-(katex|raw)|remark-(breaks|gfm|math)|streamdown|swr' +rg -n "(from\\s+['\"](${MOVED_RENDERER_PACKAGE_PATTERN})(/[^'\"]*)?['\"]|import\\s+['\"](${MOVED_RENDERER_PACKAGE_PATTERN})(/[^'\"]*)?['\"]|require\\(\\s*['\"](${MOVED_RENDERER_PACKAGE_PATTERN})(/[^'\"]*)?['\"]\\s*\\))" \ packages/desktop/src/common packages/desktop/src/process packages/desktop/src/preload packages/web-host/src packages/web-cli/src scripts ``` From 1961c0e8575b303ff02d1aad0770192a4828d036 Mon Sep 17 00:00:00 2001 From: Eva Date: Sat, 27 Jun 2026 18:55:20 +0700 Subject: [PATCH 6/7] Tighten renderer prune smoke verification --- .github/workflows/pr-checks.yml | 4 +- .../workbench-renderer-dependency-ledger.md | 2 +- scripts/evaosVerifyRendererDependencyPrune.js | 42 ++++++++++++------- ...evaosVerifyRendererDependencyPrune.test.ts | 17 ++++++-- 4 files changed, 46 insertions(+), 19 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 6d25b8c79b5..687e98e6706 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -604,6 +604,9 @@ jobs: - name: Run postinstall run: npm run postinstall || true + - name: Verify renderer dependency manifest + run: node scripts/evaosVerifyRendererDependencyPrune.js --verify-package-json + - name: Get Electron version id: electron-version shell: bash @@ -707,7 +710,6 @@ jobs: BUNDLE_ID="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP_PATH/Contents/Info.plist")" VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$APP_PATH/Contents/Info.plist")" - node scripts/evaosVerifyRendererDependencyPrune.js --verify-package-json node scripts/evaosVerifyRendererDependencyPrune.js "$APP_PATH" { diff --git a/docs/evaos/workbench-renderer-dependency-ledger.md b/docs/evaos/workbench-renderer-dependency-ledger.md index 282cf5781de..76d7b3d6d21 100644 --- a/docs/evaos/workbench-renderer-dependency-ledger.md +++ b/docs/evaos/workbench-renderer-dependency-ledger.md @@ -45,7 +45,7 @@ Local source checks: ```bash MOVED_RENDERER_PACKAGE_PATTERN='@arco-design/web-react|@codemirror/(commands|lang-css|lang-html|lang-json|lang-markdown|view)|@dnd-kit/(core|sortable|utilities)|@floating-ui/react|@icon-park/react|@monaco-editor/react|@uiw/(codemirror-extensions-langs|react-codemirror)|classnames|dayjs|diff2html|katex|mermaid|qrcode\.react|react|react-dom|react-i18next|react-markdown|react-router-dom|react-syntax-highlighter|react-virtuoso|rehype-(katex|raw)|remark-(breaks|gfm|math)|streamdown|swr' rg -n "(from\\s+['\"](${MOVED_RENDERER_PACKAGE_PATTERN})(/[^'\"]*)?['\"]|import\\s+['\"](${MOVED_RENDERER_PACKAGE_PATTERN})(/[^'\"]*)?['\"]|require\\(\\s*['\"](${MOVED_RENDERER_PACKAGE_PATTERN})(/[^'\"]*)?['\"]\\s*\\))" \ - packages/desktop/src/common packages/desktop/src/process packages/desktop/src/preload packages/web-host/src packages/web-cli/src scripts + packages/desktop/src/common packages/desktop/src/process packages/desktop/src/preload packages/web-host packages/web-cli scripts ``` Remote package proof is enforced by Thin App Smoke after `electron-builder --dir` produces an unpacked `.app`: diff --git a/scripts/evaosVerifyRendererDependencyPrune.js b/scripts/evaosVerifyRendererDependencyPrune.js index 8131f707457..9fa90e479c6 100644 --- a/scripts/evaosVerifyRendererDependencyPrune.js +++ b/scripts/evaosVerifyRendererDependencyPrune.js @@ -87,6 +87,11 @@ function packagePathExists(root, packageName) { return fs.existsSync(path.join(root, ...packageName.split('/'))); } +function packagePathExistsInAsar(asarPaths, packageName) { + const root = `/node_modules/${packageName}`; + return asarPaths.some((entryPath) => entryPath === root || entryPath.startsWith(`${root}/`)); +} + /** * Verifies that the dependency-prune package lists match package.json buckets. * @@ -125,7 +130,7 @@ function verifyRendererDependencyManifest(packageJsonPath = path.join(__dirname, * Verifies an unpacked Electron app does not raw-ship renderer-only packages. * * @param {string} appPath Path to the unpacked `.app` bundle. - * @returns {{ checkedPackages: number }} Count of renderer-only package names checked. + * @returns {{ checkedPackages: number, checkedRuntimeTransitivePackages: number }} Count of package names checked. */ function verifyRendererDependencyPrune(appPath) { if (!appPath) { @@ -141,27 +146,36 @@ function verifyRendererDependencyPrune(appPath) { const header = readAsarHeader(appAsar); const asarPaths = collectPaths(header.files); - const leakedInAsar = rendererOnlyPackages.filter((packageName) => { - const root = `/node_modules/${packageName}`; - return asarPaths.some((entryPath) => entryPath === root || entryPath.startsWith(`${root}/`)); - }); + const leakedInAsar = rendererOnlyPackages.filter((packageName) => packagePathExistsInAsar(asarPaths, packageName)); const leakedInUnpacked = rendererOnlyPackages.filter((packageName) => packagePathExists(unpackedNodeModules, packageName) ); - if (leakedInAsar.length || leakedInUnpacked.length) { - const errors = []; - if (leakedInAsar.length) { - errors.push(`Renderer-only packages leaked into app.asar: ${leakedInAsar.join(', ')}`); - } - if (leakedInUnpacked.length) { - errors.push(`Renderer-only packages leaked into app.asar.unpacked/node_modules: ${leakedInUnpacked.join(', ')}`); - } + const missingRuntimeTransitivePackages = runtimeTransitivePackages.filter( + (packageName) => + !packagePathExistsInAsar(asarPaths, packageName) && !packagePathExists(unpackedNodeModules, packageName) + ); + + const errors = []; + if (leakedInAsar.length) { + errors.push(`Renderer-only packages leaked into app.asar: ${leakedInAsar.join(', ')}`); + } + if (leakedInUnpacked.length) { + errors.push(`Renderer-only packages leaked into app.asar.unpacked/node_modules: ${leakedInUnpacked.join(', ')}`); + } + if (missingRuntimeTransitivePackages.length) { + errors.push(`Missing runtime-transitive packages: ${missingRuntimeTransitivePackages.join(', ')}`); + } + + if (errors.length) { throw new Error(errors.join('\n')); } - return { checkedPackages: rendererOnlyPackages.length }; + return { + checkedPackages: rendererOnlyPackages.length, + checkedRuntimeTransitivePackages: runtimeTransitivePackages.length, + }; } function main() { diff --git a/tests/unit/process/evaosVerifyRendererDependencyPrune.test.ts b/tests/unit/process/evaosVerifyRendererDependencyPrune.test.ts index 55812a93acc..0f7a2bb174e 100644 --- a/tests/unit/process/evaosVerifyRendererDependencyPrune.test.ts +++ b/tests/unit/process/evaosVerifyRendererDependencyPrune.test.ts @@ -12,7 +12,10 @@ const verifier = require('../../../scripts/evaosVerifyRendererDependencyPrune.js checkedRendererPackages: number; checkedRuntimeTransitivePackages: number; }; - verifyRendererDependencyPrune: (appPath: string) => { checkedPackages: number }; + verifyRendererDependencyPrune: (appPath: string) => { + checkedPackages: number; + checkedRuntimeTransitivePackages: number; + }; }; let tempDir: string | undefined; @@ -55,7 +58,7 @@ function writeFakeAsar(appPath: string, packages: string[]) { fs.writeFileSync(path.join(resources, 'app.asar'), Buffer.concat([sizePickle, headerPickle])); } -function makeApp(packages: string[] = []) { +function makeApp(packages: string[] = verifier.runtimeTransitivePackages) { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evaos-renderer-prune-')); const appPath = path.join(tempDir, 'AionUi.app'); writeFakeAsar(appPath, packages); @@ -75,6 +78,7 @@ describe('evaosVerifyRendererDependencyPrune', () => { expect(verifier.verifyRendererDependencyPrune(appPath)).toEqual({ checkedPackages: verifier.rendererOnlyPackages.length, + checkedRuntimeTransitivePackages: verifier.runtimeTransitivePackages.length, }); }); @@ -86,7 +90,7 @@ describe('evaosVerifyRendererDependencyPrune', () => { }); it('fails if any moved renderer dependency ships inside app.asar node_modules', () => { - const appPath = makeApp(verifier.rendererOnlyPackages); + const appPath = makeApp([...verifier.runtimeTransitivePackages, ...verifier.rendererOnlyPackages]); expect(() => verifier.verifyRendererDependencyPrune(appPath)).toThrow(verifier.rendererOnlyPackages[0]); @@ -111,4 +115,11 @@ describe('evaosVerifyRendererDependencyPrune', () => { expect(() => verifier.verifyRendererDependencyPrune(appPath)).toThrow(leakedPackage); }); + + it('fails if a runtime-transitive dependency is missing from the packaged app', () => { + const missingPackage = verifier.runtimeTransitivePackages[0]; + const appPath = makeApp(verifier.runtimeTransitivePackages.filter((packageName) => packageName !== missingPackage)); + + expect(() => verifier.verifyRendererDependencyPrune(appPath)).toThrow(missingPackage); + }); }); From b0207f7e6462bfcffb40381efc1db635b74349bc Mon Sep 17 00:00:00 2001 From: Eva Date: Sat, 27 Jun 2026 19:05:07 +0700 Subject: [PATCH 7/7] Cover nested renderer dependency leaks --- .../workbench-renderer-dependency-ledger.md | 2 +- scripts/evaosVerifyRendererDependencyPrune.js | 19 +++++++++++-- ...evaosVerifyRendererDependencyPrune.test.ts | 28 ++++++++++++++++--- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/docs/evaos/workbench-renderer-dependency-ledger.md b/docs/evaos/workbench-renderer-dependency-ledger.md index 76d7b3d6d21..0caf3c89e33 100644 --- a/docs/evaos/workbench-renderer-dependency-ledger.md +++ b/docs/evaos/workbench-renderer-dependency-ledger.md @@ -44,7 +44,7 @@ Local source checks: ```bash MOVED_RENDERER_PACKAGE_PATTERN='@arco-design/web-react|@codemirror/(commands|lang-css|lang-html|lang-json|lang-markdown|view)|@dnd-kit/(core|sortable|utilities)|@floating-ui/react|@icon-park/react|@monaco-editor/react|@uiw/(codemirror-extensions-langs|react-codemirror)|classnames|dayjs|diff2html|katex|mermaid|qrcode\.react|react|react-dom|react-i18next|react-markdown|react-router-dom|react-syntax-highlighter|react-virtuoso|rehype-(katex|raw)|remark-(breaks|gfm|math)|streamdown|swr' -rg -n "(from\\s+['\"](${MOVED_RENDERER_PACKAGE_PATTERN})(/[^'\"]*)?['\"]|import\\s+['\"](${MOVED_RENDERER_PACKAGE_PATTERN})(/[^'\"]*)?['\"]|require\\(\\s*['\"](${MOVED_RENDERER_PACKAGE_PATTERN})(/[^'\"]*)?['\"]\\s*\\))" \ +rg -n "(from\\s+['\"](${MOVED_RENDERER_PACKAGE_PATTERN})(/[^'\"]*)?['\"]|import\\s+['\"](${MOVED_RENDERER_PACKAGE_PATTERN})(/[^'\"]*)?['\"]|import\\s*\\(\\s*['\"](${MOVED_RENDERER_PACKAGE_PATTERN})(/[^'\"]*)?['\"]\\s*\\)|require\\(\\s*['\"](${MOVED_RENDERER_PACKAGE_PATTERN})(/[^'\"]*)?['\"]\\s*\\))" \ packages/desktop/src/common packages/desktop/src/process packages/desktop/src/preload packages/web-host packages/web-cli scripts ``` diff --git a/scripts/evaosVerifyRendererDependencyPrune.js b/scripts/evaosVerifyRendererDependencyPrune.js index 9fa90e479c6..f6a28a898b7 100644 --- a/scripts/evaosVerifyRendererDependencyPrune.js +++ b/scripts/evaosVerifyRendererDependencyPrune.js @@ -88,8 +88,23 @@ function packagePathExists(root, packageName) { } function packagePathExistsInAsar(asarPaths, packageName) { - const root = `/node_modules/${packageName}`; - return asarPaths.some((entryPath) => entryPath === root || entryPath.startsWith(`${root}/`)); + const packageParts = packageName.split('/'); + return asarPaths.some((entryPath) => { + const parts = entryPath.split('/').filter(Boolean); + for (let index = 0; index < parts.length; index += 1) { + if (parts[index] !== 'node_modules') { + continue; + } + const candidateParts = parts.slice(index + 1, index + 1 + packageParts.length); + if ( + candidateParts.length === packageParts.length && + candidateParts.every((part, partIndex) => part === packageParts[partIndex]) + ) { + return true; + } + } + return false; + }); } /** diff --git a/tests/unit/process/evaosVerifyRendererDependencyPrune.test.ts b/tests/unit/process/evaosVerifyRendererDependencyPrune.test.ts index 0f7a2bb174e..3deefd40f19 100644 --- a/tests/unit/process/evaosVerifyRendererDependencyPrune.test.ts +++ b/tests/unit/process/evaosVerifyRendererDependencyPrune.test.ts @@ -32,7 +32,17 @@ function addPackage(files: Record, packageName: string) { } } -function writeFakeAsar(appPath: string, packages: string[]) { +function addNestedPackage(files: Record, parentPackageName: string, packageName: string) { + let current = files; + for (const part of parentPackageName.split('/')) { + const entry = (current[part] ??= { files: {} }) as { files: Record }; + current = entry.files; + } + const nodeModules = (current.node_modules ??= { files: {} }) as { files: Record }; + addPackage(nodeModules.files, packageName); +} + +function writeFakeAsar(appPath: string, packages: string[], nestedPackages: string[] = []) { const resources = path.join(appPath, 'Contents', 'Resources'); fs.mkdirSync(resources, { recursive: true }); @@ -40,9 +50,12 @@ function writeFakeAsar(appPath: string, packages: string[]) { for (const packageName of packages) { addPackage(nodeModules.files, packageName); } + for (const packageName of nestedPackages) { + addNestedPackage(nodeModules.files, 'runtime-parent', packageName); + } const header = { - files: packages.length ? { node_modules: nodeModules } : { renderer: { files: {} } }, + files: packages.length || nestedPackages.length ? { node_modules: nodeModules } : { renderer: { files: {} } }, }; const json = Buffer.from(JSON.stringify(header), 'utf8'); const headerPayloadSize = 4 + align4(json.length); @@ -58,10 +71,10 @@ function writeFakeAsar(appPath: string, packages: string[]) { fs.writeFileSync(path.join(resources, 'app.asar'), Buffer.concat([sizePickle, headerPickle])); } -function makeApp(packages: string[] = verifier.runtimeTransitivePackages) { +function makeApp(packages: string[] = verifier.runtimeTransitivePackages, nestedPackages: string[] = []) { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evaos-renderer-prune-')); const appPath = path.join(tempDir, 'AionUi.app'); - writeFakeAsar(appPath, packages); + writeFakeAsar(appPath, packages, nestedPackages); return appPath; } @@ -105,6 +118,13 @@ describe('evaosVerifyRendererDependencyPrune', () => { } }); + it('fails if a moved renderer dependency ships inside nested app.asar node_modules', () => { + const leakedPackage = 'react'; + const appPath = makeApp(verifier.runtimeTransitivePackages, [leakedPackage]); + + expect(() => verifier.verifyRendererDependencyPrune(appPath)).toThrow(leakedPackage); + }); + it('fails if a moved renderer dependency ships inside app.asar.unpacked node_modules', () => { const appPath = makeApp(); const leakedPackage = 'react-router-dom';