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 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() -const lockfileCache = new Map() - -/** - * Check if a global pm is available - */ -function hasGlobalInstallation(pm: PM): Promise { - 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 { - 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 { 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> 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 = {