diff --git a/sources/Engine.ts b/sources/Engine.ts index d93501596..a9742cfac 100644 --- a/sources/Engine.ts +++ b/sources/Engine.ts @@ -93,6 +93,31 @@ export async function activatePackageManager(lastKnownGood: Record): boolean { + switch (packageManager) { + // npm/pnpm: any command with -g or --global flag + case SupportedPackageManagers.Npm: + case SupportedPackageManagers.Pnpm: + return args.includes(`-g`) || args.includes(`--global`); + + // yarn: yarn global + // Note: `yarn global` is only available in Yarn 1.x. If a newer version is used, + // Yarn itself will report an appropriate error. + case SupportedPackageManagers.Yarn: + return args[0] === `global`; + + default: + // If a new package manager is added, TypeScript will error here + // reminding us to handle it + throw new Error(`Unhandled package manager: ${packageManager satisfies never}`); + } +} + export class Engine { constructor(public config: Config = defaultConfig as Config) { } @@ -334,6 +359,12 @@ export class Engine { } } + // Global operations (install -g, uninstall -g) should be transparent + // since they operate outside of the project scope + if (!isTransparentCommand) { + isTransparentCommand = isGlobalCommand(packageManager, args); + } + const fallbackReference = isTransparentCommand ? definition.transparent.default ?? defaultVersion : defaultVersion; diff --git a/tests/main.test.ts b/tests/main.test.ts index c629f002f..0f075c612 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -621,6 +621,95 @@ it(`should allow using transparent commands on npm-configured projects`, async ( }); }); +describe(`should allow global install/uninstall commands in projects configured for a different package manager`, () => { + it(`npm install -g in yarn project`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + packageManager: `yarn@1.22.22`, + }); + + // npm install -g --help should work (we use --help to avoid actual installation) + await expect(runCli(cwd, [`npm`, `install`, `-g`, `--help`])).resolves.toMatchObject({ + exitCode: 0, + }); + }); + }); + + it(`npm uninstall --global in yarn project`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + packageManager: `yarn@1.22.22`, + }); + + await expect(runCli(cwd, [`npm`, `uninstall`, `--global`, `--help`])).resolves.toMatchObject({ + exitCode: 0, + }); + }); + }); + + it(`npm i -g in pnpm project`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + packageManager: `pnpm@9.0.0`, + }); + + await expect(runCli(cwd, [`npm`, `i`, `-g`, `--help`])).resolves.toMatchObject({ + exitCode: 0, + }); + }); + }); + + it(`pnpm add -g in yarn project`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + packageManager: `yarn@1.22.22`, + }); + + await expect(runCli(cwd, [`pnpm`, `add`, `-g`, `--help`])).resolves.toMatchObject({ + exitCode: 0, + }); + }); + }); + + it(`pnpm remove --global in npm project`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + packageManager: `npm@9.0.0`, + }); + + await expect(runCli(cwd, [`pnpm`, `remove`, `--global`, `--help`])).resolves.toMatchObject({ + exitCode: 0, + }); + }); + }); + + it(`yarn global add in npm project`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + packageManager: `npm@9.0.0`, + }); + + // yarn global add should not be blocked by project packageManager + // Note: If a Yarn version that doesn't support `global` is used, + // Yarn itself will report an appropriate error. + const result = await runCli(cwd, [`yarn`, `global`, `add`, `does-not-exist-pkg-12345`]); + expect(result.stderr).not.toContain(`This project is configured to use`); + }); + }); + + it(`yarn global remove in pnpm project`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + packageManager: `pnpm@9.0.0`, + }); + + // yarn global remove should not be blocked by project packageManager + const result = await runCli(cwd, [`yarn`, `global`, `remove`, `does-not-exist-pkg-12345`]); + expect(result.stderr).not.toContain(`This project is configured to use`); + }); + }); +}); + it(`should transparently use the preconfigured version when there is no local project`, async () => { await xfs.mktempPromise(async cwd => { await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({