Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion packages/cli-kit/src/public/node/node-package-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ import {
PackageManager,
npmLockfile,
lockfilesByManager,
_resetPackageManagerCache,
} from './node-package-manager.js'
import {captureOutput, exec} from './system.js'
import {inTemporaryDirectory, mkdir, touchFile, writeFile} from './fs.js'
import {inTemporaryDirectory, mkdir, removeFile, touchFile, writeFile} from './fs.js'
import {joinPath, dirname, normalizePath} from './path.js'
import {inferPackageManagerForGlobalCLI} from './is-global.js'
import {cacheClear} from '../../private/node/conf-store.js'
Expand Down Expand Up @@ -852,6 +853,28 @@ describe('writePackageJSON', () => {
})

describe('getPackageManager', () => {
afterEach(() => {
_resetPackageManagerCache()
})

test('memoizes the result', async () => {
await inTemporaryDirectory(async (tmpDir) => {
// Given
await writePackageJSON(tmpDir, {name: 'mock name'})
await writeFile(joinPath(tmpDir, 'yarn.lock'), '')

// When
const first = await getPackageManager(tmpDir)
// Remove the lockfile to ensure the second call doesn't find it if it's not memoized
await removeFile(joinPath(tmpDir, 'yarn.lock'))
const second = await getPackageManager(tmpDir)

// Then
expect(first).toEqual('yarn')
expect(second).toEqual('yarn')
})
})

test('finds if npm is being used', async () => {
await inTemporaryDirectory(async (tmpDir) => {
// Given — pin NPM in the temp project
Expand Down
55 changes: 45 additions & 10 deletions packages/cli-kit/src/public/node/node-package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {AbortError, BugError} from './error.js'
import {AbortController, AbortSignal} from './abort.js'
import {exec} from './system.js'
import {fileExists, readFile, writeFile, findPathUp, glob, fileExistsSync} from './fs.js'
import {dirname, joinPath} from './path.js'
import {dirname, joinPath, normalizePath} from './path.js'
import {runWithTimer} from './metadata.js'
import {inferPackageManagerForGlobalCLI} from './is-global.js'
import {outputToken, outputContent, outputDebug} from './output.js'
Expand Down Expand Up @@ -141,6 +141,19 @@ function packageManagerBinaryCommand(
}
}

/**
* Memoized value for the package manager.
*/
let memoizedPackageManager: Map<string, PackageManager> = new Map()

/**
* Resets the memoized package manager cache.
* This is useful for unit tests.
*/
export function _resetPackageManagerCache(): void {
memoizedPackageManager = new Map()
}

/**
* Returns the dependency manager used in a directory.
* Walks upward from `fromDirectory` so workspace packages (e.g. `extensions/my-fn/package.json`)
Expand All @@ -151,24 +164,46 @@ function packageManagerBinaryCommand(
* @returns The dependency manager
*/
export async function getPackageManager(fromDirectory: string): Promise<PackageManager> {
const normalizedPath = normalizePath(fromDirectory)
if (memoizedPackageManager.has(normalizedPath)) {
return memoizedPackageManager.get(normalizedPath)!
}

let result: PackageManager = 'npm'
let current = fromDirectory
outputDebug(outputContent`Looking for a lockfile in ${outputToken.path(current)}...`)
while (true) {
if (fileExistsSync(joinPath(current, yarnLockfile))) return 'yarn'
if (fileExistsSync(joinPath(current, yarnLockfile))) {
result = 'yarn'
break
}
if (fileExistsSync(joinPath(current, pnpmLockfile)) || fileExistsSync(joinPath(current, pnpmWorkspaceFile))) {
return 'pnpm'
result = 'pnpm'
break
}
if (hasBunLockfileSync(current)) {
result = 'bun'
break
}
if (fileExistsSync(joinPath(current, npmLockfile))) {
result = 'npm'
break
}
if (hasBunLockfileSync(current)) return 'bun'
if (fileExistsSync(joinPath(current, npmLockfile))) return 'npm'
const parent = dirname(current)
if (parent === current) break
if (parent === current) {
const pm: PackageManager = packageManagerFromUserAgent()
if (pm === 'unknown') {
result = 'npm'
} else {
result = pm
}
break
}
current = parent
}

const pm: PackageManager = packageManagerFromUserAgent()
if (pm !== 'unknown') return pm

return 'npm'
memoizedPackageManager.set(normalizedPath, result)
return result
}

/**
Expand Down
Loading