Skip to content

Commit 9824de7

Browse files
committed
feat: Improve package manager detection logic and enhance tests for various scenarios. Fixes #42
1 parent dd4066e commit 9824de7

File tree

2 files changed

+461
-87
lines changed

2 files changed

+461
-87
lines changed

src/utils/package-manager.ts

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,79 @@
11
import { execa, type ExecaError } from 'execa'
22
import { logger } from './logger.js'
3+
import fs from 'fs-extra'
4+
import path from 'path'
35

46
export type PackageManager = 'pnpm' | 'npm' | 'yarn'
57

68
/**
7-
* Detect which package manager is available
9+
* Validate if a string is a supported package manager
810
*/
9-
export async function detectPackageManager(): Promise<PackageManager | null> {
10-
const managers: PackageManager[] = ['pnpm', 'npm', 'yarn']
11+
function isValidPackageManager(manager: string): manager is PackageManager {
12+
return manager === 'pnpm' || manager === 'npm' || manager === 'yarn'
13+
}
14+
15+
/**
16+
* Detect which package manager to use for a project
17+
* Checks in order:
18+
* 1. packageManager field in package.json (Node.js standard)
19+
* 2. Lock files (pnpm-lock.yaml, package-lock.json, yarn.lock)
20+
* 3. Installed package managers (system-wide check)
21+
* 4. Defaults to npm if all detection fails
22+
*
23+
* @param cwd Working directory to detect package manager in (defaults to process.cwd())
24+
* @returns The detected package manager, or 'npm' as default
25+
*/
26+
export async function detectPackageManager(cwd: string = process.cwd()): Promise<PackageManager> {
27+
// 1. Check packageManager field in package.json
28+
try {
29+
const packageJsonPath = path.join(cwd, 'package.json')
30+
if (await fs.pathExists(packageJsonPath)) {
31+
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8')
32+
const packageJson = JSON.parse(packageJsonContent)
33+
34+
if (packageJson.packageManager) {
35+
// Parse "[email protected]" or "[email protected]+sha512..." -> "pnpm"
36+
const manager = packageJson.packageManager.split('@')[0]
37+
if (isValidPackageManager(manager)) {
38+
logger.debug(`Detected package manager from package.json: ${manager}`)
39+
return manager
40+
}
41+
}
42+
}
43+
} catch (error) {
44+
// If package.json doesn't exist, is malformed, or unreadable, continue to next detection method
45+
logger.debug(`Could not read packageManager from package.json: ${error instanceof Error ? error.message : 'Unknown error'}`)
46+
}
1147

48+
// 2. Check lock files (priority: pnpm > npm > yarn)
49+
const lockFiles: Array<{ file: string; manager: PackageManager }> = [
50+
{ file: 'pnpm-lock.yaml', manager: 'pnpm' },
51+
{ file: 'package-lock.json', manager: 'npm' },
52+
{ file: 'yarn.lock', manager: 'yarn' },
53+
]
54+
55+
for (const { file, manager } of lockFiles) {
56+
if (await fs.pathExists(path.join(cwd, file))) {
57+
logger.debug(`Detected package manager from lock file ${file}: ${manager}`)
58+
return manager
59+
}
60+
}
61+
62+
// 3. Check installed package managers (original behavior)
63+
const managers: PackageManager[] = ['pnpm', 'npm', 'yarn']
1264
for (const manager of managers) {
1365
try {
1466
await execa(manager, ['--version'])
67+
logger.debug(`Detected installed package manager: ${manager}`)
1568
return manager
1669
} catch {
1770
// Continue to next manager
1871
}
1972
}
2073

21-
return null
74+
// 4. Default to npm (always available in Node.js environments)
75+
logger.debug('No package manager detected, defaulting to npm')
76+
return 'npm'
2277
}
2378

2479
/**
@@ -31,11 +86,7 @@ export async function installDependencies(
3186
cwd: string,
3287
frozen: boolean = true
3388
): Promise<void> {
34-
const packageManager = await detectPackageManager()
35-
36-
if (!packageManager) {
37-
throw new Error('No package manager found (pnpm, npm, or yarn)')
38-
}
89+
const packageManager = await detectPackageManager(cwd)
3990

4091
logger.info(`Installing dependencies with ${packageManager}...`)
4192

@@ -83,11 +134,7 @@ export async function runScript(
83134
cwd: string,
84135
args: string[] = []
85136
): Promise<void> {
86-
const packageManager = await detectPackageManager()
87-
88-
if (!packageManager) {
89-
throw new Error('No package manager found (pnpm, npm, or yarn)')
90-
}
137+
const packageManager = await detectPackageManager(cwd)
91138

92139
const command = packageManager === 'npm' ? ['run', scriptName] : [scriptName]
93140

0 commit comments

Comments
 (0)