Skip to content
Open
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
31 changes: 31 additions & 0 deletions sources/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,31 @@ export async function activatePackageManager(lastKnownGood: Record<string, strin
await createLastKnownGoodFile(lastKnownGood);
}

/**
* Checks if the command is a global operation.
* Global operations should be transparent since they operate outside
* of the project scope.
*/
function isGlobalCommand(packageManager: SupportedPackageManagers, args: Array<string>): 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 <command>
// 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) {
}
Expand Down Expand Up @@ -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;
Expand Down
89 changes: 89 additions & 0 deletions tests/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down