mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
The `getPkgManager` helpers previously consulted the filesystem first for files hinting at the used package manager. Only if none were found did we check `process.env.npm_config_user_agent` and then shelled out to testing which package manager is available. This change makes all helpers check the `npm_config_user_agent` environment variable first. The variable is set by the package manager that invoked the process, so when present it is both the cheapest and the most direct signal, avoiding filesystem access and subprocess spawns entirely. `create-next-app` already used this user-agent-first approach and is unchanged. The check order is now consistent across all three implementations: yarn, pnpm, bun, npm for the user agent (the `packages/next` variant has no `bun` in its `PackageManager` type and falls through to lockfile detection there), and the same order for the codemod's lockfile walk, which previously prioritized `package-lock.json`. Because the invoking package manager now takes precedence over lockfile inference, running `npx next ...` or `npx @next/codemod ...` inside a pnpm or yarn app is now detected as npm. Use `pnpx` (or `pnpm dlx`) and `yarn dlx` instead so the detected package manager matches the project.
217 lines
5.3 KiB
TypeScript
217 lines
5.3 KiB
TypeScript
import findUp from 'find-up'
|
|
import execa from 'execa'
|
|
import { execSync } from 'node:child_process'
|
|
import { basename } from 'node:path'
|
|
|
|
export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun'
|
|
|
|
/**
|
|
* Get the full version string for the given package manager.
|
|
*
|
|
* First tries to parse from `npm_config_user_agent` (e.g., "pnpm/9.13.2 npm/? ..."),
|
|
* then falls back to spawning `<packageManager> --version`.
|
|
*
|
|
* Returns null if unable to determine the version.
|
|
*
|
|
* Mirrors `packages/create-next-app/helpers/get-pkg-manager.ts`.
|
|
*/
|
|
export function getPackageManagerVersion(
|
|
packageManager: PackageManager
|
|
): string | null {
|
|
const userAgent = process.env.npm_config_user_agent || ''
|
|
const userAgentMatch = userAgent.match(
|
|
new RegExp(`${packageManager}/([\\d.]+[\\w.-]*)`)
|
|
)
|
|
if (userAgentMatch) {
|
|
return userAgentMatch[1]
|
|
}
|
|
|
|
try {
|
|
const version = execSync(`${packageManager} --version`, {
|
|
encoding: 'utf8',
|
|
stdio: ['pipe', 'pipe', 'ignore'],
|
|
}).trim()
|
|
if (/^\d+\.\d+\.\d+/.test(version)) {
|
|
return version
|
|
}
|
|
} catch {
|
|
// package manager not available or failed to run
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Get the major version of pnpm being used.
|
|
* Returns null if unable to determine the version.
|
|
*/
|
|
export function getPnpmMajorVersion(): number | null {
|
|
const version = getPackageManagerVersion('pnpm')
|
|
if (!version) return null
|
|
const major = parseInt(version.split('.')[0], 10)
|
|
return Number.isNaN(major) ? null : major
|
|
}
|
|
|
|
export function getPkgManager(baseDir: string): PackageManager {
|
|
try {
|
|
const userAgent = process.env.npm_config_user_agent
|
|
if (userAgent) {
|
|
if (userAgent.startsWith('yarn')) {
|
|
return 'yarn'
|
|
} else if (userAgent.startsWith('pnpm')) {
|
|
return 'pnpm'
|
|
} else if (userAgent.startsWith('bun')) {
|
|
return 'bun'
|
|
} else if (userAgent.startsWith('npm')) {
|
|
return 'npm'
|
|
}
|
|
}
|
|
const lockFile = findUp.sync(
|
|
[
|
|
'yarn.lock',
|
|
'pnpm-lock.yaml',
|
|
'bun.lock',
|
|
'bun.lockb',
|
|
'package-lock.json',
|
|
],
|
|
{ cwd: baseDir }
|
|
)
|
|
if (lockFile) {
|
|
switch (basename(lockFile)) {
|
|
case 'yarn.lock':
|
|
return 'yarn'
|
|
case 'pnpm-lock.yaml':
|
|
return 'pnpm'
|
|
case 'bun.lock':
|
|
case 'bun.lockb':
|
|
return 'bun'
|
|
case 'package-lock.json':
|
|
return 'npm'
|
|
default:
|
|
return 'npm'
|
|
}
|
|
}
|
|
// No lock file found, default to npm
|
|
return 'npm'
|
|
} catch {
|
|
return 'npm'
|
|
}
|
|
}
|
|
|
|
export function uninstallPackage(
|
|
packageToUninstall: string,
|
|
pkgManager?: PackageManager
|
|
) {
|
|
pkgManager ??= getPkgManager(process.cwd())
|
|
if (!pkgManager) throw new Error('Failed to find package manager')
|
|
|
|
let command = 'uninstall'
|
|
if (pkgManager === 'yarn') {
|
|
command = 'remove'
|
|
}
|
|
|
|
try {
|
|
execa.sync(pkgManager, [command, packageToUninstall], {
|
|
stdio: 'inherit',
|
|
shell: true,
|
|
})
|
|
} catch (error) {
|
|
throw new Error(
|
|
`Failed to uninstall "${packageToUninstall}". Please uninstall it manually.`,
|
|
{ cause: error }
|
|
)
|
|
}
|
|
}
|
|
|
|
const ADD_CMD_FLAG = {
|
|
npm: 'install',
|
|
yarn: 'add',
|
|
pnpm: 'add',
|
|
bun: 'add',
|
|
}
|
|
|
|
const DEV_DEP_FLAG = {
|
|
npm: '--save-dev',
|
|
yarn: '--dev',
|
|
pnpm: '--save-dev',
|
|
bun: '--dev',
|
|
}
|
|
|
|
export function installPackages(
|
|
packageToInstall: string[],
|
|
options: {
|
|
packageManager?: PackageManager
|
|
silent?: boolean
|
|
dev?: boolean
|
|
} = {}
|
|
) {
|
|
if (packageToInstall.length === 0) return
|
|
|
|
const {
|
|
packageManager = getPkgManager(process.cwd()),
|
|
silent = false,
|
|
dev = false,
|
|
} = options
|
|
|
|
if (!packageManager) throw new Error('Failed to find package manager')
|
|
|
|
const addCmd = ADD_CMD_FLAG[packageManager]
|
|
const devDepFlag = dev ? DEV_DEP_FLAG[packageManager] : undefined
|
|
|
|
const installFlags = [addCmd]
|
|
if (devDepFlag) {
|
|
installFlags.push(devDepFlag)
|
|
}
|
|
try {
|
|
execa.sync(packageManager, [...installFlags, ...packageToInstall], {
|
|
// Keeping stderr since it'll likely be relevant later when it fails.
|
|
stdio: silent ? ['ignore', 'ignore', 'inherit'] : 'inherit',
|
|
shell: true,
|
|
})
|
|
} catch (error) {
|
|
throw new Error(
|
|
`Failed to install "${packageToInstall}". Please install it manually.`,
|
|
{ cause: error }
|
|
)
|
|
}
|
|
}
|
|
|
|
export function runInstallation(
|
|
packageManager: PackageManager,
|
|
options: { cwd: string }
|
|
) {
|
|
try {
|
|
execa.sync(packageManager, ['install'], {
|
|
cwd: options.cwd,
|
|
env: {
|
|
...process.env,
|
|
// In case NODE_ENV=production is set, we still want dev dependencies to
|
|
// be installed. Otherwise we won't be able to check for peer dependencies.
|
|
// --production=false is not implemented by every package manager.
|
|
NODE_ENV: 'development',
|
|
},
|
|
stdio: 'inherit',
|
|
shell: true,
|
|
})
|
|
} catch (error) {
|
|
throw new Error('Failed to install dependencies', { cause: error })
|
|
}
|
|
}
|
|
|
|
export function addPackageDependency(
|
|
packageJson: Record<string, any>,
|
|
name: string,
|
|
version: string,
|
|
dev: boolean
|
|
): void {
|
|
if (dev) {
|
|
packageJson.devDependencies = packageJson.devDependencies || {}
|
|
} else {
|
|
packageJson.dependencies = packageJson.dependencies || {}
|
|
}
|
|
|
|
const deps = dev ? packageJson.devDependencies : packageJson.dependencies
|
|
|
|
deps[name] = version
|
|
}
|