diff --git a/packages/app/src/cli/models/project/project-integration.test.ts b/packages/app/src/cli/models/project/project-integration.test.ts index d9eca152934..a72994a26da 100644 --- a/packages/app/src/cli/models/project/project-integration.test.ts +++ b/packages/app/src/cli/models/project/project-integration.test.ts @@ -79,6 +79,8 @@ dev = "npm run dev" // package.json (needed by the loader) await writeFile(joinPath(dir, 'package.json'), JSON.stringify({name: 'test-app', dependencies: {}})) + // Pin npm: getPackageManager walks up to ancestors if no lockfile is found + await writeFile(joinPath(dir, 'package-lock.json'), '') } // Load specifications once — this is expensive (loads all extension specs from disk) diff --git a/packages/app/src/cli/services/app/config/use.test.ts b/packages/app/src/cli/services/app/config/use.test.ts index 21f6f46ec55..428e0c0a41b 100644 --- a/packages/app/src/cli/services/app/config/use.test.ts +++ b/packages/app/src/cli/services/app/config/use.test.ts @@ -45,6 +45,7 @@ describe('use', () => { } writeFileSync(joinPath(tmp, 'package.json'), '{}') writeFileSync(joinPath(tmp, 'shopify.app.toml'), '') + writeFileSync(joinPath(tmp, 'package-lock.json'), '') // When await use(options) diff --git a/packages/cli-kit/src/public/node/node-package-manager.test.ts b/packages/cli-kit/src/public/node/node-package-manager.test.ts index 5c22f54ecde..8516249743f 100644 --- a/packages/cli-kit/src/public/node/node-package-manager.test.ts +++ b/packages/cli-kit/src/public/node/node-package-manager.test.ts @@ -20,6 +20,7 @@ import { checkForCachedNewVersion, inferPackageManager, PackageManager, + npmLockfile, } from './node-package-manager.js' import {captureOutput, exec} from './system.js' import {inTemporaryDirectory, mkdir, touchFile, writeFile} from './fs.js' @@ -845,8 +846,9 @@ describe('writePackageJSON', () => { describe('getPackageManager', () => { test('finds if npm is being used', async () => { await inTemporaryDirectory(async (tmpDir) => { - // Given + // Given — pin NPM in the temp project await writePackageJSON(tmpDir, {name: 'mock name'}) + await writeFile(joinPath(tmpDir, npmLockfile), '') // Then const packageManager = await getPackageManager(tmpDir) @@ -878,6 +880,19 @@ describe('getPackageManager', () => { }) }) + test('finds pnpm from a nested workspace package when the lockfile is only at the repo root', async () => { + await inTemporaryDirectory(async (tmpDir) => { + await writePackageJSON(tmpDir, {name: 'root'}) + await writeFile(joinPath(tmpDir, 'pnpm-lock.yaml'), '') + const nested = joinPath(tmpDir, 'extensions', 'cart-transformer') + await mkdir(nested) + await writePackageJSON(nested, {name: 'cart-transformer'}) + + const packageManager = await getPackageManager(nested) + expect(packageManager).toEqual('pnpm') + }) + }) + test('falls back to packageManagerFromUserAgent when no package.json is found', async () => { await inTemporaryDirectory(async (tmpDir) => { // Given — no package.json in tmpDir, stub user agent to yarn diff --git a/packages/cli-kit/src/public/node/node-package-manager.ts b/packages/cli-kit/src/public/node/node-package-manager.ts index bdbce5aff7e..b35afb8b6d5 100644 --- a/packages/cli-kit/src/public/node/node-package-manager.ts +++ b/packages/cli-kit/src/public/node/node-package-manager.ts @@ -1,7 +1,7 @@ import {AbortError, BugError} from './error.js' import {AbortController, AbortSignal} from './abort.js' import {exec} from './system.js' -import {fileExists, readFile, writeFile, findPathUp, glob} from './fs.js' +import {fileExists, readFile, writeFile, findPathUp, glob, fileExistsSync} from './fs.js' import {dirname, joinPath} from './path.js' import {runWithTimer} from './metadata.js' import {inferPackageManagerForGlobalCLI} from './is-global.js' @@ -111,21 +111,29 @@ export function packageManagerFromUserAgent(env = process.env): PackageManager { /** * Returns the dependency manager used in a directory. + * Walks upward from `fromDirectory` so workspace packages (e.g. `extensions/my-fn/package.json`) + * still resolve to the repo root lockfile (`pnpm-lock.yaml`). + * If no lockfile is found, it falls back to the package manager from the user agent. + * If the package manager from the user agent is unknown, it returns 'npm'. * @param fromDirectory - The starting directory * @returns The dependency manager */ export async function getPackageManager(fromDirectory: string): Promise { - const packageJsonPath = await findPathUp('package.json', {cwd: fromDirectory, type: 'file'}) - if (!packageJsonPath) { - return packageManagerFromUserAgent() + 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, pnpmLockfile))) return 'pnpm' + if (fileExistsSync(joinPath(current, bunLockfile))) return 'bun' + if (fileExistsSync(joinPath(current, npmLockfile))) return 'npm' + const parent = dirname(current) + if (parent === current) break + current = parent } - const directory = dirname(packageJsonPath) - outputDebug(outputContent`Obtaining the dependency manager in directory ${outputToken.path(directory)}...`) + const pm: PackageManager = packageManagerFromUserAgent() + if (pm !== 'unknown') return pm - if (await fileExists(joinPath(directory, yarnLockfile))) return 'yarn' - if (await fileExists(joinPath(directory, pnpmLockfile))) return 'pnpm' - if (await fileExists(joinPath(directory, bunLockfile))) return 'bun' return 'npm' }