Repository: electron-userland/electron-builder
Pull request #9067: refactor: update package manager detection and improve type handling
Author: login: beyondkmp
Labels: name: app-builder-lib
Created: 2025-04-29T06:50:03Z | Merged: 2025-05-04T01:55:26Z

fix https://github.com/electron-userland/electron-builder/issues/8970#issuecomment-2830421527

- Replaced the `detect` and `getPackageManagerVersion` functions with a new `detectPackageManager` function for better clarity and type safety.
- Updated the `PM` type to an enum for improved maintainability.
- Adjusted the logic in `getCollectorByPackageManager` and `installDependencies` to utilize the new detection method.
- Cleaned up unused code and comments in `packageManager.ts` and `yarn.ts` files.


Diff:
diff --git a/.changeset/blue-icons-push.md b/.changeset/blue-icons-push.md
new file mode 100644
index 00000000000..795dc2bf098
--- /dev/null
+++ b/.changeset/blue-icons-push.md
@@ -0,0 +1,5 @@
+---
+"app-builder-lib": patch
+---
+
+refactor: update package manager detection and improve type handling
diff --git a/packages/app-builder-lib/src/node-module-collector/index.ts b/packages/app-builder-lib/src/node-module-collector/index.ts
index 2e760b0567e..5feff0809d3 100644
--- a/packages/app-builder-lib/src/node-module-collector/index.ts
+++ b/packages/app-builder-lib/src/node-module-collector/index.ts
@@ -1,7 +1,7 @@
 import { NpmNodeModulesCollector } from "./npmNodeModulesCollector"
 import { PnpmNodeModulesCollector } from "./pnpmNodeModulesCollector"
 import { YarnNodeModulesCollector } from "./yarnNodeModulesCollector"
-import { detect, PM, getPackageManagerVersion } from "./packageManager"
+import { detectPackageManager, PM, getPackageManagerCommand } from "./packageManager"
 import { NodeModuleInfo } from "./types"
 import { exec } from "builder-util"
 
@@ -13,16 +13,16 @@ async function isPnpmProjectHoisted(rootDir: string) {
 }
 
 export async function getCollectorByPackageManager(rootDir: string) {
-  const manager: PM = await detect({ cwd: rootDir })
+  const manager: PM = detectPackageManager(rootDir)
   switch (manager) {
-    case "pnpm":
+    case PM.PNPM:
       if (await isPnpmProjectHoisted(rootDir)) {
         return new NpmNodeModulesCollector(rootDir)
       }
       return new PnpmNodeModulesCollector(rootDir)
-    case "npm":
+    case PM.NPM:
       return new NpmNodeModulesCollector(rootDir)
-    case "yarn":
+    case PM.YARN:
       return new YarnNodeModulesCollector(rootDir)
     default:
       return new NpmNodeModulesCollector(rootDir)
@@ -34,4 +34,4 @@ export async function getNodeModules(rootDir: string): Promise<NodeModuleInfo[]>
   return collector.getNodeModules()
 }
 
-export { detect, getPackageManagerVersion, PM }
+export { detectPackageManager, PM, getPackageManagerCommand }
diff --git a/packages/app-builder-lib/src/node-module-collector/packageManager.ts b/packages/app-builder-lib/src/node-module-collector/packageManager.ts
index 5f8b99469d3..ffdf5d271f2 100644
--- a/packages/app-builder-lib/src/node-module-collector/packageManager.ts
+++ b/packages/app-builder-lib/src/node-module-collector/packageManager.ts
@@ -1,95 +1,103 @@
-// copy from https://github.com/egoist/detect-package-manager/blob/main/src/index.ts
-// and merge https://github.com/egoist/detect-package-manager/pull/9 to support Monorepo
-import { resolve, dirname } from "path"
-import { exec, exists } from "builder-util"
-
-export type PM = "npm" | "yarn" | "pnpm" | "bun"
-
-const cache = new Map()
-const globalInstallationCache = new Map<string, boolean>()
-const lockfileCache = new Map<string, PM>()
-
-/**
- * Check if a global pm is available
- */
-function hasGlobalInstallation(pm: PM): Promise<boolean> {
-  const key = `has_global_${pm}`
-  if (globalInstallationCache.has(key)) {
-    return Promise.resolve(globalInstallationCache.get(key)!)
-  }
+import * as path from "path"
+import * as fs from "fs"
 
-  return exec(pm, ["--version"], { shell: true })
-    .then(res => {
-      return /^\d+.\d+.\d+$/.test(res)
-    })
-    .then(value => {
-      globalInstallationCache.set(key, value)
-      return value
-    })
-    .catch(() => false)
+export enum PM {
+  NPM = "npm",
+  YARN = "yarn",
+  PNPM = "pnpm",
+  YARN_BERRY = "yarn-berry",
 }
 
-function getTypeofLockFile(cwd = process.cwd()): Promise<PM> {
-  const key = `lockfile_${cwd}`
-  if (lockfileCache.has(key)) {
-    return Promise.resolve(lockfileCache.get(key)!)
-  }
+function detectPackageManagerByEnv(): PM {
+  if (process.env.npm_config_user_agent) {
+    const userAgent = process.env.npm_config_user_agent
 
-  return Promise.all([
-    exists(resolve(cwd, "yarn.lock")),
-    exists(resolve(cwd, "package-lock.json")),
-    exists(resolve(cwd, "pnpm-lock.yaml")),
-    exists(resolve(cwd, "bun.lockb")),
-  ]).then(([isYarn, _, isPnpm, isBun]) => {
-    let value: PM
-
-    if (isYarn) {
-      value = "yarn"
-    } else if (isPnpm) {
-      value = "pnpm"
-    } else if (isBun) {
-      value = "bun"
-    } else {
-      value = "npm"
+    if (userAgent.includes("pnpm")) {
+      return PM.PNPM
     }
 
-    cache.set(key, value)
-    return value
-  })
-}
+    if (userAgent.includes("yarn")) {
+      if (userAgent.includes("yarn/")) {
+        const version = userAgent.match(/yarn\/(\d+)\./)
+        if (version && parseInt(version[1]) >= 2) {
+          return PM.YARN_BERRY
+        }
+      }
+      return PM.YARN
+    }
 
-export const detect = async ({ cwd, includeGlobalBun }: { cwd?: string; includeGlobalBun?: boolean } = {}) => {
-  let type = await getTypeofLockFile(cwd)
-  if (type) {
-    return type
+    if (userAgent.includes("npm")) {
+      return PM.NPM
+    }
   }
 
-  let tmpCwd = cwd || "."
-  for (let i = 1; i <= 5; i++) {
-    tmpCwd = dirname(tmpCwd)
-    type = await getTypeofLockFile(tmpCwd)
-    if (type) {
-      return type
+  if (process.env.npm_execpath) {
+    const execPath = process.env.npm_execpath.toLowerCase()
+
+    if (execPath.includes("pnpm")) {
+      return PM.PNPM
+    }
+
+    if (execPath.includes("yarn")) {
+      if (execPath.includes("berry") || process.env.YARN_VERSION?.startsWith("2.") || process.env.YARN_VERSION?.startsWith("3.")) {
+        return PM.YARN_BERRY
+      }
+      return PM.YARN
+    }
+
+    if (execPath.includes("npm")) {
+      return PM.NPM
     }
   }
 
-  if (await hasGlobalInstallation("yarn")) {
-    return "yarn"
+  if (process.env.PNPM_HOME) {
+    return PM.PNPM
   }
-  if (await hasGlobalInstallation("pnpm")) {
-    return "yarn"
+
+  if (process.env.YARN_REGISTRY) {
+    if (process.env.YARN_VERSION?.startsWith("2.") || process.env.YARN_VERSION?.startsWith("3.")) {
+      return PM.YARN_BERRY
+    }
+    return PM.YARN
   }
 
-  if (includeGlobalBun && (await hasGlobalInstallation("bun"))) {
-    return "bun"
+  if (process.env.npm_package_json) {
+    return PM.NPM
   }
-  return "npm"
+
+  // return default
+  return PM.NPM
 }
 
-export function getPackageManagerVersion(pm: PM) {
-  return exec(pm, ["--version"], { shell: true }).then(res => res.trim())
+export function getPackageManagerCommand(pm: PM) {
+  let cmd = pm
+  if (pm === PM.YARN_BERRY || process.env.FORCE_YARN === "true") {
+    cmd = PM.YARN
+  }
+  return `${cmd}${process.platform === "win32" ? ".cmd" : ""}`
 }
 
-export function clearCache() {
-  return cache.clear()
+export function detectPackageManager(cwd: string) {
+  const isYarnLockFileExists = fs.existsSync(path.join(cwd, "yarn.lock"))
+  const isPnpmLockFileExists = fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))
+  const isNpmLockFileExists = fs.existsSync(path.join(cwd, "package-lock.json"))
+
+  if (isYarnLockFileExists && !isPnpmLockFileExists && !isNpmLockFileExists) {
+    // check if yarn is berry
+    const pm = detectPackageManagerByEnv()
+    if (pm === PM.YARN_BERRY) {
+      return PM.YARN_BERRY
+    }
+    return PM.YARN
+  }
+
+  if (isPnpmLockFileExists && !isYarnLockFileExists && !isNpmLockFileExists) {
+    return PM.PNPM
+  }
+
+  if (isNpmLockFileExists && !isYarnLockFileExists && !isPnpmLockFileExists) {
+    return PM.NPM
+  }
+  // if there are no lock files or multiple lock files, return the package manager from env
+  return detectPackageManagerByEnv()
 }
diff --git a/packages/app-builder-lib/src/util/yarn.ts b/packages/app-builder-lib/src/util/yarn.ts
index 1a4d8342c84..c5894cd50b6 100644
--- a/packages/app-builder-lib/src/util/yarn.ts
+++ b/packages/app-builder-lib/src/util/yarn.ts
@@ -7,7 +7,7 @@ import { homedir } from "os"
 import * as path from "path"
 import { Configuration } from "../configuration"
 import { executeAppBuilderAndWriteJson } from "./appBuilder"
-import { PM, detect, getPackageManagerVersion } from "../node-module-collector"
+import { PM, detectPackageManager, getPackageManagerCommand } from "../node-module-collector"
 import { NodeModuleDirInfo } from "./packageDependencies"
 import { rebuild as remoteRebuild } from "./rebuild/rebuild"
 
@@ -78,38 +78,25 @@ export function getGypEnv(frameworkInfo: DesktopFrameworkInfo, platform: NodeJS.
   }
 }
 
-async function checkYarnBerry(pm: PM) {
-  if (pm !== "yarn") {
-    return false
-  }
-  const version = await getPackageManagerVersion(pm)
-  if (version == null || version.split(".").length < 1) {
-    return false
-  }
-
-  return version.split(".")[0] >= "2"
-}
-
 async function installDependencies(config: Configuration, { appDir, projectDir }: DirectoryPaths, options: RebuildOptions): Promise<any> {
   const platform = options.platform || process.platform
   const arch = options.arch || process.arch
   const additionalArgs = options.additionalArgs
 
-  const pm = await detect({ cwd: projectDir })
+  const pm = detectPackageManager(projectDir)
   log.info({ pm, platform, arch, projectDir, appDir }, `installing production dependencies`)
   const execArgs = ["install"]
-  const isYarnBerry = await checkYarnBerry(pm)
-  if (!isYarnBerry) {
+  if (pm === PM.YARN_BERRY) {
     if (process.env.NPM_NO_BIN_LINKS === "true") {
       execArgs.push("--no-bin-links")
     }
   }
 
-  if (!isRunningYarn(pm)) {
+  if (pm === PM.YARN) {
     execArgs.push("--prefer-offline")
   }
 
-  const execPath = getPackageToolPath(pm)
+  const execPath = getPackageManagerCommand(pm)
 
   if (additionalArgs != null) {
     execArgs.push(...additionalArgs)
@@ -142,20 +129,6 @@ export async function nodeGypRebuild(platform: NodeJS.Platform, arch: string, fr
   }
   await spawn(nodeGyp, args, { env: getGypEnv(frameworkInfo, platform, arch, true) })
 }
-
-function getPackageToolPath(pm: PM) {
-  let cmd = pm
-  if (process.env.FORCE_YARN === "true") {
-    cmd = "yarn"
-  }
-  return `${cmd}${process.platform === "win32" ? ".cmd" : ""}`
-}
-
-function isRunningYarn(pm: PM) {
-  const userAgent = process.env.npm_config_user_agent
-  return process.env.FORCE_YARN === "true" || pm === "yarn" || (userAgent != null && /\byarn\b/.test(userAgent))
-}
-
 export interface RebuildOptions {
   frameworkInfo: DesktopFrameworkInfo
   productionDeps: Lazy<Array<NodeModuleDirInfo>>
diff --git a/test/src/BuildTest.ts b/test/src/BuildTest.ts
index 48ab85750c1..f577bd8c49f 100644
--- a/test/src/BuildTest.ts
+++ b/test/src/BuildTest.ts
@@ -363,8 +363,9 @@ test.ifDevOrLinuxCi("win smart unpack", ({ expect }) => {
     },
     {
       isInstallDepsBefore: true,
-      projectDirCreated: projectDir => {
+      projectDirCreated: async projectDir => {
         p = projectDir
+        process.env.npm_config_user_agent = "npm"
         return packageJson(it => {
           it.dependencies = {
             debug: "3.1.0",
@@ -440,14 +441,17 @@ test.ifDevOrLinuxCi("posix smart unpack", ({ expect }) =>
     },
     {
       isInstallDepsBefore: true,
-      projectDirCreated: packageJson(it => {
-        it.dependencies = {
-          debug: "4.1.1",
-          "edge-cs": "1.2.1",
-          keytar: "7.9.0",
-          three: "0.160.0",
-        }
-      }),
+      projectDirCreated: projectDir => {
+        process.env.npm_config_user_agent = "npm"
+        return packageJson(it => {
+          it.dependencies = {
+            debug: "4.1.1",
+            "edge-cs": "1.2.1",
+            keytar: "7.9.0",
+            three: "0.160.0",
+          }
+        })(projectDir)
+      },
       packed: async context => {
         expect(context.packager.appInfo.copyright).toBe("Copyright © 2018 Foo Bar")
         await verifySmartUnpack(expect, context.getResources(Platform.LINUX), async asarFs => {
diff --git a/test/src/HoistedNodeModuleTest.ts b/test/src/HoistedNodeModuleTest.ts
index 9c00882f2fd..bf806fff67f 100644
--- a/test/src/HoistedNodeModuleTest.ts
+++ b/test/src/HoistedNodeModuleTest.ts
@@ -178,7 +178,7 @@ describe("isInstallDepsBefore=true", { sequential: true }, () => {
       },
       {
         isInstallDepsBefore: true,
-        projectDirCreated: projectDir => {
+        projectDirCreated: async projectDir => {
           const subAppDir = path.join(projectDir, "packages", "test-app")
           return modifyPackageJson(subAppDir, data => {
             data.name = "@scope/xxx-app"
diff --git a/test/src/globTest.ts b/test/src/globTest.ts
index eee6bb71af3..4075c26a3b1 100644
--- a/test/src/globTest.ts
+++ b/test/src/globTest.ts
@@ -145,6 +145,7 @@ describe("isInstallDepsBefore=true", { sequential: true }, () => {
       {
         isInstallDepsBefore: true,
         projectDirCreated: async projectDir => {
+          await outputFile(path.join(projectDir, "package-lock.json"), "")
           await modifyPackageJson(projectDir, data => {
             data.dependencies = {
               debug: "4.1.1",
@@ -207,7 +208,7 @@ describe("isInstallDepsBefore=true", { sequential: true }, () => {
       },
       {
         isInstallDepsBefore: true,
-        projectDirCreated: projectDir => {
+        projectDirCreated: async projectDir => {
           return Promise.all([
             modifyPackageJson(projectDir, data => {
               //noinspection SpellCheckingInspection
@@ -241,15 +242,17 @@ describe("isInstallDepsBefore=true", { sequential: true }, () => {
       },
       {
         isInstallDepsBefore: true,
-        projectDirCreated: projectDir =>
-          modifyPackageJson(projectDir, data => {
+        projectDirCreated: async projectDir => {
+          await outputFile(path.join(projectDir, "package-lock.json"), "")
+          return modifyPackageJson(projectDir, data => {
             //noinspection SpellCheckingInspection
             data.dependencies = {
               "ci-info": "2.0.0",
               // this contains string-width-cjs 4.2.3
               "@isaacs/cliui": "8.0.2",
             }
-          }),
+          })
+        },
         packed: context => {
           return assertThat(expect, path.join(context.getResources(Platform.LINUX), "app", "node_modules")).doesNotExist()
         },
@@ -269,12 +272,14 @@ describe("isInstallDepsBefore=true", { sequential: true }, () => {
       },
       {
         isInstallDepsBefore: true,
-        projectDirCreated: projectDir =>
-          modifyPackageJson(projectDir, data => {
+        projectDirCreated: async projectDir => {
+          await outputFile(path.join(projectDir, "package-lock.json"), "")
+          return modifyPackageJson(projectDir, data => {
             data.dependencies = {
               "ci-info": "2.0.0",
             }
-          }),
+          })
+        },
         packed: async context => {
           const nodeModulesNode = (await readAsar(path.join(context.getResources(Platform.LINUX), "app.asar"))).getNode("node_modules")
           expect(removeUnstableProperties(nodeModulesNode)).toMatchSnapshot()
diff --git a/test/src/ignoreTest.ts b/test/src/ignoreTest.ts
index 773c8065047..06866780f5a 100644
--- a/test/src/ignoreTest.ts
+++ b/test/src/ignoreTest.ts
@@ -90,7 +90,8 @@ test.ifNotCiMac.sequential("ignore node_modules dev dep", ({ expect }) =>
     },
     {
       isInstallDepsBefore: true,
-      projectDirCreated: projectDir => {
+      projectDirCreated: async projectDir => {
+        await outputFile(path.join(projectDir, "package-lock.json"), "")
         return Promise.all([
           modifyPackageJson(projectDir, data => {
             data.devDependencies = {
@@ -119,7 +120,8 @@ test.ifDevOrLinuxCi.sequential("copied sub node_modules of the rootDir/node_modu
     },
     {
       isInstallDepsBefore: true,
-      projectDirCreated: projectDir => {
+      projectDirCreated: async projectDir => {
+        await outputFile(path.join(projectDir, "package-lock.json"), "")
         return Promise.all([
           modifyPackageJson(projectDir, data => {
             data.dependencies = {


Reviews:
user: login: mmaietta
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T14:57:58Z
user: login: beyondkmp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T02:38:33Z
user: login: mmaietta
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-01T14:56:43Z
user: login: beyondkmp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-03T23:40:52Z
user: login: mmaietta
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-04T01:06:37Z
user: login: beyondkmp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-04T01:29:19Z
user: login: mmaietta
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-04T01:41:50Z
user: login: cinderisles
state: COMMENTED
author_association: NONE
submitted_at: 2025-08-29T16:36:26Z
user: login: avarayr
state: COMMENTED
author_association: NONE
submitted_at: 2025-08-29T16:58:22Z

Review comments:
diff_hunk: @@ -146,16 +133,11 @@ export async function nodeGypRebuild(platform: NodeJS.Platform, arch: string, fr
 function getPackageToolPath(pm: PM) {
   let cmd = pm
   if (process.env.FORCE_YARN === "true") {
-    cmd = "yarn"
+    cmd = PM.YARN
   }
   return `${cmd}${process.platform === "win32" ? ".cmd" : ""}`
path: packages/app-builder-lib/src/util/yarn.ts
user: login: mmaietta
body: Just to double check. Does this still work if `PM.YARN_BERRY` is used here? Is the `cmd` on Windows `yarn-berry.cmd`?
created_at: 2025-04-30T14:57:26Z
author_association: COLLABORATOR
diff_hunk: @@ -146,16 +133,11 @@ export async function nodeGypRebuild(platform: NodeJS.Platform, arch: string, fr
 function getPackageToolPath(pm: PM) {
   let cmd = pm
   if (process.env.FORCE_YARN === "true") {
-    cmd = "yarn"
+    cmd = PM.YARN
   }
   return `${cmd}${process.platform === "win32" ? ".cmd" : ""}`
path: packages/app-builder-lib/src/util/yarn.ts
user: login: beyondkmp
body: No, it's still `yarn.cmd`.

![image](https://github.com/user-attachments/assets/6cac5325-5ae9-42f0-8982-e26241c7a5a2)

created_at: 2025-05-01T02:38:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -146,16 +133,11 @@ export async function nodeGypRebuild(platform: NodeJS.Platform, arch: string, fr
 function getPackageToolPath(pm: PM) {
   let cmd = pm
   if (process.env.FORCE_YARN === "true") {
-    cmd = "yarn"
+    cmd = PM.YARN
   }
   return `${cmd}${process.platform === "win32" ? ".cmd" : ""}`
path: packages/app-builder-lib/src/util/yarn.ts
user: login: mmaietta
body: Then this logic here seems to be incorrect? If `pm = PM.YARN_BERRY`, then it'll evaluate to `yarn-berry${process.platform === "win32" ? ".cmd" : ""}`, right?
created_at: 2025-05-01T14:56:43Z
author_association: COLLABORATOR
diff_hunk: @@ -146,16 +133,11 @@ export async function nodeGypRebuild(platform: NodeJS.Platform, arch: string, fr
 function getPackageToolPath(pm: PM) {
   let cmd = pm
   if (process.env.FORCE_YARN === "true") {
-    cmd = "yarn"
+    cmd = PM.YARN
   }
   return `${cmd}${process.platform === "win32" ? ".cmd" : ""}`
path: packages/app-builder-lib/src/util/yarn.ts
user: login: beyondkmp
body: @mmaietta  Sorry for the misunderstanding. Yarn Berry still uses `yarn.cmd`, not `yarn-berry.cmd`. This has already been verified.
created_at: 2025-05-03T23:40:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -146,16 +133,11 @@ export async function nodeGypRebuild(platform: NodeJS.Platform, arch: string, fr
 function getPackageToolPath(pm: PM) {
   let cmd = pm
   if (process.env.FORCE_YARN === "true") {
-    cmd = "yarn"
+    cmd = PM.YARN
   }
   return `${cmd}${process.platform === "win32" ? ".cmd" : ""}`
path: packages/app-builder-lib/src/util/yarn.ts
user: login: mmaietta
body: This logic here still doesn't seem right to me.

```
function getPackageToolPath(pm: PM) {
  let cmd = pm
  if (process.env.FORCE_YARN === "true") {
    cmd = "yarn"
    cmd = PM.YARN
  }
  return `${cmd}${process.platform === "win32" ? ".cmd" : ""}`
}
```
When `pm = PM.YARN_BERRY && platform === win32`, it returns -> `return "yarn-berry.cmd"`

created_at: 2025-05-04T01:06:37Z
author_association: COLLABORATOR
diff_hunk: @@ -146,16 +133,11 @@ export async function nodeGypRebuild(platform: NodeJS.Platform, arch: string, fr
 function getPackageToolPath(pm: PM) {
   let cmd = pm
   if (process.env.FORCE_YARN === "true") {
-    cmd = "yarn"
+    cmd = PM.YARN
   }
   return `${cmd}${process.platform === "win32" ? ".cmd" : ""}`
path: packages/app-builder-lib/src/util/yarn.ts
user: login: beyondkmp
body: My bad, I misunderstood. I’ve added this function to `packageManager`, so others can directly call it when needed, without having to create their own.
created_at: 2025-05-04T01:29:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,95 +1,103 @@
-// copy from https://github.com/egoist/detect-package-manager/blob/main/src/index.ts
-// and merge https://github.com/egoist/detect-package-manager/pull/9 to support Monorepo
-import { resolve, dirname } from "path"
-import { exec, exists } from "builder-util"
-
-export type PM = "npm" | "yarn" | "pnpm" | "bun"
-
-const cache = new Map()
-const globalInstallationCache = new Map<string, boolean>()
-const lockfileCache = new Map<string, PM>()
-
-/**
- * Check if a global pm is available
- */
-function hasGlobalInstallation(pm: PM): Promise<boolean> {
-  const key = `has_global_${pm}`
-  if (globalInstallationCache.has(key)) {
-    return Promise.resolve(globalInstallationCache.get(key)!)
-  }
+import * as path from "path"
+import * as fs from "fs"
 
-  return exec(pm, ["--version"], { shell: true })
-    .then(res => {
-      return /^\d+.\d+.\d+$/.test(res)
-    })
-    .then(value => {
-      globalInstallationCache.set(key, value)
-      return value
-    })
-    .catch(() => false)
+export enum PM {
+  NPM = "npm",
+  YARN = "yarn",
+  PNPM = "pnpm",
+  YARN_BERRY = "yarn-berry",
 }
 
-function getTypeofLockFile(cwd = process.cwd()): Promise<PM> {
-  const key = `lockfile_${cwd}`
-  if (lockfileCache.has(key)) {
-    return Promise.resolve(lockfileCache.get(key)!)
-  }
+function detectPackageManagerByEnv(): PM {
+  if (process.env.npm_config_user_agent) {
+    const userAgent = process.env.npm_config_user_agent
 
-  return Promise.all([
-    exists(resolve(cwd, "yarn.lock")),
-    exists(resolve(cwd, "package-lock.json")),
-    exists(resolve(cwd, "pnpm-lock.yaml")),
-    exists(resolve(cwd, "bun.lockb")),
path: packages/app-builder-lib/src/node-module-collector/packageManager.ts
user: login: cinderisles
body: Bun 1.2 changed the default lockfile format to text-based, so the extension is `.lock` and not just `.lockb`. I think the binary version `.lockb` is still supported, just not the default anymore, so there should be a check here for both

https://bun.com/blog/bun-lock-text-lockfile
created_at: 2025-08-29T16:36:26Z
author_association: NONE
diff_hunk: @@ -1,95 +1,103 @@
-// copy from https://github.com/egoist/detect-package-manager/blob/main/src/index.ts
-// and merge https://github.com/egoist/detect-package-manager/pull/9 to support Monorepo
-import { resolve, dirname } from "path"
-import { exec, exists } from "builder-util"
-
-export type PM = "npm" | "yarn" | "pnpm" | "bun"
-
-const cache = new Map()
-const globalInstallationCache = new Map<string, boolean>()
-const lockfileCache = new Map<string, PM>()
-
-/**
- * Check if a global pm is available
- */
-function hasGlobalInstallation(pm: PM): Promise<boolean> {
-  const key = `has_global_${pm}`
-  if (globalInstallationCache.has(key)) {
-    return Promise.resolve(globalInstallationCache.get(key)!)
-  }
+import * as path from "path"
+import * as fs from "fs"
 
-  return exec(pm, ["--version"], { shell: true })
-    .then(res => {
-      return /^\d+.\d+.\d+$/.test(res)
-    })
-    .then(value => {
-      globalInstallationCache.set(key, value)
-      return value
-    })
-    .catch(() => false)
+export enum PM {
+  NPM = "npm",
+  YARN = "yarn",
+  PNPM = "pnpm",
+  YARN_BERRY = "yarn-berry",
 }
 
-function getTypeofLockFile(cwd = process.cwd()): Promise<PM> {
-  const key = `lockfile_${cwd}`
-  if (lockfileCache.has(key)) {
-    return Promise.resolve(lockfileCache.get(key)!)
-  }
+function detectPackageManagerByEnv(): PM {
+  if (process.env.npm_config_user_agent) {
+    const userAgent = process.env.npm_config_user_agent
 
-  return Promise.all([
-    exists(resolve(cwd, "yarn.lock")),
-    exists(resolve(cwd, "package-lock.json")),
-    exists(resolve(cwd, "pnpm-lock.yaml")),
-    exists(resolve(cwd, "bun.lockb")),
path: packages/app-builder-lib/src/node-module-collector/packageManager.ts
user: login: avarayr
body: great point @cinderisles 
i've added a companion PR to the active https://github.com/electron-userland/electron-builder/pull/9262 
hopefully we'll get bun back thru that

if not i'll try to make a separate PR to do that
created_at: 2025-08-29T16:58:21Z
author_association: NONE

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-04-29T06:50:07Z
body: ###  🦋  Changeset detected

Latest commit: 210f3cf53d8ff825bda10c49e7078aa79f3fb880

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 8 packages</summary>

  | Name                              | Type  |
| --------------------------------- | ----- |
| app-builder-lib                   | Patch |
| dmg-builder                       | Patch |
| electron-builder-squirrel-windows | Patch |
| electron-builder                  | Patch |
| electron-forge-maker-appimage     | Patch |
| electron-forge-maker-nsis-web     | Patch |
| electron-forge-maker-nsis         | Patch |
| electron-forge-maker-snap         | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/beyondkmp/electron-builder/new/detectPackageManager?filename=.changeset/odd-lamps-brush.md&value=---%0A%22app-builder-lib%22%3A%20patch%0A%22%40electron-builder%2Ftest%22%3A%20patch%0A---%0A%0Arefactor%3A%20update%20package%20manager%20detection%20and%20improve%20type%20handling%0A)


author_association: NONE
user: login: beyondkmp
created_at: 2025-05-01T02:44:31Z
body: @mmaietta 

Should we directly determine the package manager based on the env, and completely remove the logic that determines it based on the lock file? Could you check if this might cause any other issues?
author_association: CONTRIBUTOR
user: login: mmaietta
created_at: 2025-05-01T14:59:24Z
body: Hmmm, I think we should emulate the approach that electron-builder took prior to the `PM` refactor. IIRC, it only accepted `npm`/`yarn` though, so we are technically expanding the featureset of detecting of the package manager. I'm not sure what errors could occur if we only used env, but envs can differ across local dev vs. build machine, not sure if we want to only take into account env. If multiple lockfiles are detected, then we should use env, otherwise fall back to lockfile logic?
author_association: COLLABORATOR
user: login: beyondkmp
created_at: 2025-05-03T23:47:18Z
body: @mmaietta The current logic has been changed: if there is only one lock file, the lock file will be used to determine the package manager (PM). If there are multiple lock files or no lock file, the environment variables (`env`) will be used instead.
author_association: CONTRIBUTOR
user: login: cinderisles
created_at: 2025-08-22T17:32:32Z
body: @beyondkmp @mmaietta didn't this PR effectively remove support for Bun without notice? Bun can be used as just a drop-in replacement for npm much like using pnpm with `--shamefully-hoist` and I only see removals of references to bun in this PR. What I'd like to be able to do is just force Bun usage and have electron-builder treat it like regular npm
author_association: NONE
user: login: avarayr
created_at: 2025-08-29T16:27:20Z
body: I'm curious to understand how the removal of bun slipped thru the code review, as this is actively breaking builds in repos that use bun workspaces instead of npm

author_association: NONE
user: login: avarayr
created_at: 2025-08-29T16:30:14Z
body: <img width="1313" height="863" alt="image" src="https://github.com/user-attachments/assets/5e569f5a-afee-4c65-bd3d-38787daa93bc" />

installs initiated with `bun` in repos that use bun workspaces are erroneously defaulting to `yarn` because this pull request removed bun detection
author_association: NONE