Skip to content

Commit 312938d

Browse files
authored
fix: update package manager detection and improve type handling (#9067)
fixes #8970 (comment)
1 parent 6f3aec8 commit 312938d

File tree

8 files changed

+128
-131
lines changed

8 files changed

+128
-131
lines changed

.changeset/blue-icons-push.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"app-builder-lib": patch
3+
---
4+
5+
refactor: update package manager detection and improve type handling

packages/app-builder-lib/src/node-module-collector/index.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { NpmNodeModulesCollector } from "./npmNodeModulesCollector"
22
import { PnpmNodeModulesCollector } from "./pnpmNodeModulesCollector"
33
import { YarnNodeModulesCollector } from "./yarnNodeModulesCollector"
4-
import { detect, PM, getPackageManagerVersion } from "./packageManager"
4+
import { detectPackageManager, PM, getPackageManagerCommand } from "./packageManager"
55
import { NodeModuleInfo } from "./types"
66
import { exec } from "builder-util"
77

@@ -13,16 +13,16 @@ async function isPnpmProjectHoisted(rootDir: string) {
1313
}
1414

1515
export async function getCollectorByPackageManager(rootDir: string) {
16-
const manager: PM = await detect({ cwd: rootDir })
16+
const manager: PM = detectPackageManager(rootDir)
1717
switch (manager) {
18-
case "pnpm":
18+
case PM.PNPM:
1919
if (await isPnpmProjectHoisted(rootDir)) {
2020
return new NpmNodeModulesCollector(rootDir)
2121
}
2222
return new PnpmNodeModulesCollector(rootDir)
23-
case "npm":
23+
case PM.NPM:
2424
return new NpmNodeModulesCollector(rootDir)
25-
case "yarn":
25+
case PM.YARN:
2626
return new YarnNodeModulesCollector(rootDir)
2727
default:
2828
return new NpmNodeModulesCollector(rootDir)
@@ -34,4 +34,4 @@ export async function getNodeModules(rootDir: string): Promise<NodeModuleInfo[]>
3434
return collector.getNodeModules()
3535
}
3636

37-
export { detect, getPackageManagerVersion, PM }
37+
export { detectPackageManager, PM, getPackageManagerCommand }
Lines changed: 82 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,103 @@
1-
// copy from https://github.com/egoist/detect-package-manager/blob/main/src/index.ts
2-
// and merge https://github.com/egoist/detect-package-manager/pull/9 to support Monorepo
3-
import { resolve, dirname } from "path"
4-
import { exec, exists } from "builder-util"
5-
6-
export type PM = "npm" | "yarn" | "pnpm" | "bun"
7-
8-
const cache = new Map()
9-
const globalInstallationCache = new Map<string, boolean>()
10-
const lockfileCache = new Map<string, PM>()
11-
12-
/**
13-
* Check if a global pm is available
14-
*/
15-
function hasGlobalInstallation(pm: PM): Promise<boolean> {
16-
const key = `has_global_${pm}`
17-
if (globalInstallationCache.has(key)) {
18-
return Promise.resolve(globalInstallationCache.get(key)!)
19-
}
1+
import * as path from "path"
2+
import * as fs from "fs"
203

21-
return exec(pm, ["--version"], { shell: true })
22-
.then(res => {
23-
return /^\d+.\d+.\d+$/.test(res)
24-
})
25-
.then(value => {
26-
globalInstallationCache.set(key, value)
27-
return value
28-
})
29-
.catch(() => false)
4+
export enum PM {
5+
NPM = "npm",
6+
YARN = "yarn",
7+
PNPM = "pnpm",
8+
YARN_BERRY = "yarn-berry",
309
}
3110

32-
function getTypeofLockFile(cwd = process.cwd()): Promise<PM> {
33-
const key = `lockfile_${cwd}`
34-
if (lockfileCache.has(key)) {
35-
return Promise.resolve(lockfileCache.get(key)!)
36-
}
11+
function detectPackageManagerByEnv(): PM {
12+
if (process.env.npm_config_user_agent) {
13+
const userAgent = process.env.npm_config_user_agent
3714

38-
return Promise.all([
39-
exists(resolve(cwd, "yarn.lock")),
40-
exists(resolve(cwd, "package-lock.json")),
41-
exists(resolve(cwd, "pnpm-lock.yaml")),
42-
exists(resolve(cwd, "bun.lockb")),
43-
]).then(([isYarn, _, isPnpm, isBun]) => {
44-
let value: PM
45-
46-
if (isYarn) {
47-
value = "yarn"
48-
} else if (isPnpm) {
49-
value = "pnpm"
50-
} else if (isBun) {
51-
value = "bun"
52-
} else {
53-
value = "npm"
15+
if (userAgent.includes("pnpm")) {
16+
return PM.PNPM
5417
}
5518

56-
cache.set(key, value)
57-
return value
58-
})
59-
}
19+
if (userAgent.includes("yarn")) {
20+
if (userAgent.includes("yarn/")) {
21+
const version = userAgent.match(/yarn\/(\d+)\./)
22+
if (version && parseInt(version[1]) >= 2) {
23+
return PM.YARN_BERRY
24+
}
25+
}
26+
return PM.YARN
27+
}
6028

61-
export const detect = async ({ cwd, includeGlobalBun }: { cwd?: string; includeGlobalBun?: boolean } = {}) => {
62-
let type = await getTypeofLockFile(cwd)
63-
if (type) {
64-
return type
29+
if (userAgent.includes("npm")) {
30+
return PM.NPM
31+
}
6532
}
6633

67-
let tmpCwd = cwd || "."
68-
for (let i = 1; i <= 5; i++) {
69-
tmpCwd = dirname(tmpCwd)
70-
type = await getTypeofLockFile(tmpCwd)
71-
if (type) {
72-
return type
34+
if (process.env.npm_execpath) {
35+
const execPath = process.env.npm_execpath.toLowerCase()
36+
37+
if (execPath.includes("pnpm")) {
38+
return PM.PNPM
39+
}
40+
41+
if (execPath.includes("yarn")) {
42+
if (execPath.includes("berry") || process.env.YARN_VERSION?.startsWith("2.") || process.env.YARN_VERSION?.startsWith("3.")) {
43+
return PM.YARN_BERRY
44+
}
45+
return PM.YARN
46+
}
47+
48+
if (execPath.includes("npm")) {
49+
return PM.NPM
7350
}
7451
}
7552

76-
if (await hasGlobalInstallation("yarn")) {
77-
return "yarn"
53+
if (process.env.PNPM_HOME) {
54+
return PM.PNPM
7855
}
79-
if (await hasGlobalInstallation("pnpm")) {
80-
return "yarn"
56+
57+
if (process.env.YARN_REGISTRY) {
58+
if (process.env.YARN_VERSION?.startsWith("2.") || process.env.YARN_VERSION?.startsWith("3.")) {
59+
return PM.YARN_BERRY
60+
}
61+
return PM.YARN
8162
}
8263

83-
if (includeGlobalBun && (await hasGlobalInstallation("bun"))) {
84-
return "bun"
64+
if (process.env.npm_package_json) {
65+
return PM.NPM
8566
}
86-
return "npm"
67+
68+
// return default
69+
return PM.NPM
8770
}
8871

89-
export function getPackageManagerVersion(pm: PM) {
90-
return exec(pm, ["--version"], { shell: true }).then(res => res.trim())
72+
export function getPackageManagerCommand(pm: PM) {
73+
let cmd = pm
74+
if (pm === PM.YARN_BERRY || process.env.FORCE_YARN === "true") {
75+
cmd = PM.YARN
76+
}
77+
return `${cmd}${process.platform === "win32" ? ".cmd" : ""}`
9178
}
9279

93-
export function clearCache() {
94-
return cache.clear()
80+
export function detectPackageManager(cwd: string) {
81+
const isYarnLockFileExists = fs.existsSync(path.join(cwd, "yarn.lock"))
82+
const isPnpmLockFileExists = fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))
83+
const isNpmLockFileExists = fs.existsSync(path.join(cwd, "package-lock.json"))
84+
85+
if (isYarnLockFileExists && !isPnpmLockFileExists && !isNpmLockFileExists) {
86+
// check if yarn is berry
87+
const pm = detectPackageManagerByEnv()
88+
if (pm === PM.YARN_BERRY) {
89+
return PM.YARN_BERRY
90+
}
91+
return PM.YARN
92+
}
93+
94+
if (isPnpmLockFileExists && !isYarnLockFileExists && !isNpmLockFileExists) {
95+
return PM.PNPM
96+
}
97+
98+
if (isNpmLockFileExists && !isYarnLockFileExists && !isPnpmLockFileExists) {
99+
return PM.NPM
100+
}
101+
// if there are no lock files or multiple lock files, return the package manager from env
102+
return detectPackageManagerByEnv()
95103
}

packages/app-builder-lib/src/util/yarn.ts

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { homedir } from "os"
77
import * as path from "path"
88
import { Configuration } from "../configuration"
99
import { executeAppBuilderAndWriteJson } from "./appBuilder"
10-
import { PM, detect, getPackageManagerVersion } from "../node-module-collector"
10+
import { PM, detectPackageManager, getPackageManagerCommand } from "../node-module-collector"
1111
import { NodeModuleDirInfo } from "./packageDependencies"
1212
import { rebuild as remoteRebuild } from "./rebuild/rebuild"
1313

@@ -78,38 +78,25 @@ export function getGypEnv(frameworkInfo: DesktopFrameworkInfo, platform: NodeJS.
7878
}
7979
}
8080

81-
async function checkYarnBerry(pm: PM) {
82-
if (pm !== "yarn") {
83-
return false
84-
}
85-
const version = await getPackageManagerVersion(pm)
86-
if (version == null || version.split(".").length < 1) {
87-
return false
88-
}
89-
90-
return version.split(".")[0] >= "2"
91-
}
92-
9381
async function installDependencies(config: Configuration, { appDir, projectDir }: DirectoryPaths, options: RebuildOptions): Promise<any> {
9482
const platform = options.platform || process.platform
9583
const arch = options.arch || process.arch
9684
const additionalArgs = options.additionalArgs
9785

98-
const pm = await detect({ cwd: projectDir })
86+
const pm = detectPackageManager(projectDir)
9987
log.info({ pm, platform, arch, projectDir, appDir }, `installing production dependencies`)
10088
const execArgs = ["install"]
101-
const isYarnBerry = await checkYarnBerry(pm)
102-
if (!isYarnBerry) {
89+
if (pm === PM.YARN_BERRY) {
10390
if (process.env.NPM_NO_BIN_LINKS === "true") {
10491
execArgs.push("--no-bin-links")
10592
}
10693
}
10794

108-
if (!isRunningYarn(pm)) {
95+
if (pm === PM.YARN) {
10996
execArgs.push("--prefer-offline")
11097
}
11198

112-
const execPath = getPackageToolPath(pm)
99+
const execPath = getPackageManagerCommand(pm)
113100

114101
if (additionalArgs != null) {
115102
execArgs.push(...additionalArgs)
@@ -142,20 +129,6 @@ export async function nodeGypRebuild(platform: NodeJS.Platform, arch: string, fr
142129
}
143130
await spawn(nodeGyp, args, { env: getGypEnv(frameworkInfo, platform, arch, true) })
144131
}
145-
146-
function getPackageToolPath(pm: PM) {
147-
let cmd = pm
148-
if (process.env.FORCE_YARN === "true") {
149-
cmd = "yarn"
150-
}
151-
return `${cmd}${process.platform === "win32" ? ".cmd" : ""}`
152-
}
153-
154-
function isRunningYarn(pm: PM) {
155-
const userAgent = process.env.npm_config_user_agent
156-
return process.env.FORCE_YARN === "true" || pm === "yarn" || (userAgent != null && /\byarn\b/.test(userAgent))
157-
}
158-
159132
export interface RebuildOptions {
160133
frameworkInfo: DesktopFrameworkInfo
161134
productionDeps: Lazy<Array<NodeModuleDirInfo>>

test/src/BuildTest.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -363,8 +363,9 @@ test.ifDevOrLinuxCi("win smart unpack", ({ expect }) => {
363363
},
364364
{
365365
isInstallDepsBefore: true,
366-
projectDirCreated: projectDir => {
366+
projectDirCreated: async projectDir => {
367367
p = projectDir
368+
process.env.npm_config_user_agent = "npm"
368369
return packageJson(it => {
369370
it.dependencies = {
370371
debug: "3.1.0",
@@ -440,14 +441,17 @@ test.ifDevOrLinuxCi("posix smart unpack", ({ expect }) =>
440441
},
441442
{
442443
isInstallDepsBefore: true,
443-
projectDirCreated: packageJson(it => {
444-
it.dependencies = {
445-
debug: "4.1.1",
446-
"edge-cs": "1.2.1",
447-
keytar: "7.9.0",
448-
three: "0.160.0",
449-
}
450-
}),
444+
projectDirCreated: projectDir => {
445+
process.env.npm_config_user_agent = "npm"
446+
return packageJson(it => {
447+
it.dependencies = {
448+
debug: "4.1.1",
449+
"edge-cs": "1.2.1",
450+
keytar: "7.9.0",
451+
three: "0.160.0",
452+
}
453+
})(projectDir)
454+
},
451455
packed: async context => {
452456
expect(context.packager.appInfo.copyright).toBe("Copyright © 2018 Foo Bar")
453457
await verifySmartUnpack(expect, context.getResources(Platform.LINUX), async asarFs => {

test/src/HoistedNodeModuleTest.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ describe("isInstallDepsBefore=true", { sequential: true }, () => {
178178
},
179179
{
180180
isInstallDepsBefore: true,
181-
projectDirCreated: projectDir => {
181+
projectDirCreated: async projectDir => {
182182
const subAppDir = path.join(projectDir, "packages", "test-app")
183183
return modifyPackageJson(subAppDir, data => {
184184
data.name = "@scope/xxx-app"

test/src/globTest.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ describe("isInstallDepsBefore=true", { sequential: true }, () => {
145145
{
146146
isInstallDepsBefore: true,
147147
projectDirCreated: async projectDir => {
148+
await outputFile(path.join(projectDir, "package-lock.json"), "")
148149
await modifyPackageJson(projectDir, data => {
149150
data.dependencies = {
150151
debug: "4.1.1",
@@ -207,7 +208,7 @@ describe("isInstallDepsBefore=true", { sequential: true }, () => {
207208
},
208209
{
209210
isInstallDepsBefore: true,
210-
projectDirCreated: projectDir => {
211+
projectDirCreated: async projectDir => {
211212
return Promise.all([
212213
modifyPackageJson(projectDir, data => {
213214
//noinspection SpellCheckingInspection
@@ -241,15 +242,17 @@ describe("isInstallDepsBefore=true", { sequential: true }, () => {
241242
},
242243
{
243244
isInstallDepsBefore: true,
244-
projectDirCreated: projectDir =>
245-
modifyPackageJson(projectDir, data => {
245+
projectDirCreated: async projectDir => {
246+
await outputFile(path.join(projectDir, "package-lock.json"), "")
247+
return modifyPackageJson(projectDir, data => {
246248
//noinspection SpellCheckingInspection
247249
data.dependencies = {
248250
"ci-info": "2.0.0",
249251
// this contains string-width-cjs 4.2.3
250252
"@isaacs/cliui": "8.0.2",
251253
}
252-
}),
254+
})
255+
},
253256
packed: context => {
254257
return assertThat(expect, path.join(context.getResources(Platform.LINUX), "app", "node_modules")).doesNotExist()
255258
},
@@ -269,12 +272,14 @@ describe("isInstallDepsBefore=true", { sequential: true }, () => {
269272
},
270273
{
271274
isInstallDepsBefore: true,
272-
projectDirCreated: projectDir =>
273-
modifyPackageJson(projectDir, data => {
275+
projectDirCreated: async projectDir => {
276+
await outputFile(path.join(projectDir, "package-lock.json"), "")
277+
return modifyPackageJson(projectDir, data => {
274278
data.dependencies = {
275279
"ci-info": "2.0.0",
276280
}
277-
}),
281+
})
282+
},
278283
packed: async context => {
279284
const nodeModulesNode = (await readAsar(path.join(context.getResources(Platform.LINUX), "app.asar"))).getNode("node_modules")
280285
expect(removeUnstableProperties(nodeModulesNode)).toMatchSnapshot()

0 commit comments

Comments
 (0)