diff --git a/.github/actions/desktop-build-setup/action.yml b/.github/actions/desktop-build-setup/action.yml index 4f0e1db5f5..2c1d3931b6 100644 --- a/.github/actions/desktop-build-setup/action.yml +++ b/.github/actions/desktop-build-setup/action.yml @@ -16,8 +16,12 @@ runs: - name: Install dependencies shell: bash - run: pnpm install --node-linker=hoisted + run: pnpm install --node-linker=hoisted --config.enable-global-virtual-store=false + env: + NODE_OPTIONS: --max-old-space-size=8192 - name: Install deps on Desktop shell: bash - run: npm run install-isolated --prefix=./apps/desktop + run: pnpm --dir ./apps/desktop install --node-linker=hoisted --config.enable-global-virtual-store=false + env: + NODE_OPTIONS: --max-old-space-size=8192 diff --git a/.github/workflows/pr-build-desktop.yml b/.github/workflows/pr-build-desktop.yml index bfceed9974..4907999b1c 100644 --- a/.github/workflows/pr-build-desktop.yml +++ b/.github/workflows/pr-build-desktop.yml @@ -31,7 +31,7 @@ jobs: uses: ./.github/actions/setup-env - name: Install deps - run: pnpm install + run: pnpm install --config.enable-global-virtual-store=false env: NODE_OPTIONS: --max-old-space-size=8192 diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index 4a45e5a86d..12d66c623c 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -68,7 +68,7 @@ jobs: node-version: ${{ env.NODE_VERSION }} - name: Install deps - run: pnpm install + run: pnpm install --config.enable-global-virtual-store=false - name: Lint run: bun run lint diff --git a/.github/workflows/release-desktop-canary.yml b/.github/workflows/release-desktop-canary.yml index 01e7a94536..e03dfb87df 100644 --- a/.github/workflows/release-desktop-canary.yml +++ b/.github/workflows/release-desktop-canary.yml @@ -200,7 +200,7 @@ jobs: node-version: ${{ env.NODE_VERSION }} - name: Install deps - run: pnpm install + run: pnpm install --config.enable-global-virtual-store=false - name: Lint run: bun run lint diff --git a/apps/desktop/electron-builder.mjs b/apps/desktop/electron-builder.mjs index 736a65a454..ed012a118b 100644 --- a/apps/desktop/electron-builder.mjs +++ b/apps/desktop/electron-builder.mjs @@ -130,6 +130,11 @@ const config = { ); console.info('✅ CLI bundle copied to resources/bin/lobe-cli.js'); }, + // Native dependencies are rebuilt during the isolated desktop install and + // copied explicitly by the hooks here. Returning false marks node_modules as + // externally handled, so electron-builder does not invoke its pnpm collector + // during packaging. + beforeBuild: () => false, /** * AfterPack hook for post-processing: * 1. Copy native modules to asar.unpacked (resolving pnpm symlinks) @@ -290,7 +295,6 @@ const config = { { arch: [arch === 'arm64' ? 'arm64' : 'x64'], target: 'zip' }, ], }, - npmRebuild: true, nsis: { allowToChangeInstallationDirectory: true, artifactName: '${productName}-${version}-setup.${ext}', diff --git a/apps/desktop/native-deps.config.mjs b/apps/desktop/native-deps.config.mjs index 8cf353d0cd..1ba59a763a 100644 --- a/apps/desktop/native-deps.config.mjs +++ b/apps/desktop/native-deps.config.mjs @@ -40,6 +40,39 @@ export const nativeModules = [ 'node-screenshots', ]; +const optionalNativePackagePrefixes = new Map([ + ['@napi-rs/canvas', ['@napi-rs/canvas-']], + ['node-screenshots', ['node-screenshots-']], +]); + +const runtimeOptionalDependencies = new Map([['get-windows', ['@mapbox/node-pre-gyp']]]); + +const runtimeDependencyOverrides = new Map([ + // get-windows imports @mapbox/node-pre-gyp at runtime only for find(). + // Keep the narrow find() dependency path and avoid shipping its install, + // publish, and CLI dependency tree into app.asar. + ['@mapbox/node-pre-gyp', ['detect-libc', 'nopt', 'npmlog', 'semver']], +]); + +function getRuntimeDependencies(moduleName, dependencies) { + return runtimeDependencyOverrides.get(moduleName) || Object.keys(dependencies); +} + +function getRuntimeOptionalDependencies(moduleName, optionalDependencies) { + const prefixes = optionalNativePackagePrefixes.get(moduleName); + const dependencies = [...(runtimeOptionalDependencies.get(moduleName) || [])]; + + if (prefixes) { + dependencies.push( + ...Object.keys(optionalDependencies).filter((dep) => + prefixes.some((prefix) => dep.startsWith(prefix)), + ), + ); + } + + return dependencies; +} + /** * Recursively resolve all dependencies of a module * @param {string} moduleName - The module to resolve @@ -73,13 +106,15 @@ function resolveDependencies( const optionalDependencies = packageJson.optionalDependencies || {}; // Resolve regular dependencies - for (const dep of Object.keys(dependencies)) { + for (const dep of getRuntimeDependencies(moduleName, dependencies)) { resolveDependencies(dep, visited, nodeModulesPath); } - // Also resolve optional dependencies (important for native modules like @napi-rs/canvas - // which have platform-specific binaries in optional deps) - for (const dep of Object.keys(optionalDependencies)) { + // Only include optional packages that are runtime native binaries for the + // module itself. Native packages can also list install-time toolchains such + // as node-gyp or node-pre-gyp as optional deps; those must not be copied or + // externalized into the packaged Electron app. + for (const dep of getRuntimeOptionalDependencies(moduleName, optionalDependencies)) { resolveDependencies(dep, visited, nodeModulesPath); } } catch {