diff --git a/sources/commands/InstallGlobal.ts b/sources/commands/InstallGlobal.ts index ade5446cf..ec29a3c38 100644 --- a/sources/commands/InstallGlobal.ts +++ b/sources/commands/InstallGlobal.ts @@ -2,6 +2,7 @@ import {Command, Option, UsageError} from 'clipanion'; import fs from 'fs'; import path from 'path'; +import {getLastKnownGood} from '../Engine.ts'; import * as folderUtils from '../folderUtils.ts'; import * as specUtils from '../specUtils.ts'; import type {Descriptor, Locator} from '../types.ts'; @@ -66,8 +67,21 @@ export class InstallGlobalCommand extends BaseCommand { if (resolved === null) throw new UsageError(`Failed to successfully resolve '${descriptor.range}' to a valid ${descriptor.name} release`); - this.log(resolved); - await this.context.engine.ensurePackageManager(resolved); + const lastKnownGood = this.cacheOnly ? null : await getLastKnownGood(); + const isCurrentVersion = lastKnownGood?.[resolved.name] === resolved.reference; + + if (!isCurrentVersion) + this.log(resolved); + + const info = await this.context.engine.ensurePackageManager(resolved); + + if (isCurrentVersion) { + if (info.isAlreadyInstalled) { + this.context.stdout.write(`${resolved.name}@${resolved.reference} is the current version\n`); + } else { + this.log(resolved); + } + } if (!this.cacheOnly) { await this.context.engine.activatePackageManager(resolved); diff --git a/sources/corepackUtils.ts b/sources/corepackUtils.ts index d38df1c7b..9023b788d 100644 --- a/sources/corepackUtils.ts +++ b/sources/corepackUtils.ts @@ -224,6 +224,7 @@ export async function installVersion(installTarget: string, locator: Locator, {s hash: corepackData.hash as string, location: installFolder, bin: corepackData.bin, + isAlreadyInstalled: true, }; } catch (err) { if (nodeUtils.isNodeError(err) && err.code !== `ENOENT`) { @@ -374,6 +375,7 @@ export async function installVersion(installTarget: string, locator: Locator, {s location: installFolder, bin, hash: serializedHash, + isAlreadyInstalled: false, }; } diff --git a/sources/types.ts b/sources/types.ts index 9fca1dc40..4ca86ef8c 100644 --- a/sources/types.ts +++ b/sources/types.ts @@ -53,6 +53,8 @@ export interface InstallSpec { location: string; bin?: BinList | BinSpec; hash: string; + /** Whether the package manager was reused from the cache without downloading it. */ + isAlreadyInstalled: boolean; } export interface DownloadSpec { diff --git a/tests/InstallGlobal.test.ts b/tests/InstallGlobal.test.ts new file mode 100644 index 000000000..e9b39069e --- /dev/null +++ b/tests/InstallGlobal.test.ts @@ -0,0 +1,156 @@ +import {ppath, xfs, npath} from '@yarnpkg/fslib'; +import process from 'node:process'; +import {describe, beforeEach, it, expect} from 'vitest'; + +import {runCli} from './_runCli.ts'; + +beforeEach(async () => { + const home = await xfs.mktempPromise(); + + process.env.COREPACK_HOME = npath.fromPortablePath(home); + process.env.COREPACK_DEFAULT_TO_LATEST = `0`; + + return async () => { + await xfs.removePromise(home, {recursive: true}); + }; +}); + +describe(`InstallGlobalCommand`, () => { + it.each([ + `1.22.4`, + `1.22.4+sha224.0d6eecaf4d82ec12566fdd97143794d0f0c317e0d652bd4d1b305430`, + ])(`should report a cached and globally active version on repeated installs (%s)`, async version => { + await xfs.mktempPromise(async cwd => { + await expect(runCli(cwd, [`install`, `--global`, `yarn@${version}`])).resolves.toEqual({ + exitCode: 0, + stdout: `Installing yarn@${version}...\n`, + stderr: ``, + }); + + const lastKnownGoodFile = ppath.join(npath.toPortablePath(process.env.COREPACK_HOME!), `lastKnownGood.json`); + const before = await xfs.statPromise(lastKnownGoodFile); + process.env.COREPACK_ENABLE_NETWORK = `0`; + + await expect(runCli(cwd, [`install`, `-g`, `yarn@${version}`])).resolves.toEqual({ + exitCode: 0, + stdout: `yarn@${version} is the current version\n`, + stderr: ``, + }); + + const after = await xfs.statPromise(lastKnownGoodFile); + expect(after.mtimeMs).toBe(before.mtimeMs); + await expect(xfs.readJsonPromise(lastKnownGoodFile)).resolves.toEqual({yarn: version}); + await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toEqual({ + exitCode: 0, + stdout: `1.22.4\n`, + stderr: ``, + }); + }); + }); + + it(`should report installation when activating a different cached version`, async () => { + await xfs.mktempPromise(async cwd => { + for (const version of [`1.22.4`, `1.0.0`]) { + await expect(runCli(cwd, [`install`, `-g`, `yarn@${version}`])).resolves.toEqual({ + exitCode: 0, + stdout: `Installing yarn@${version}...\n`, + stderr: ``, + }); + } + + process.env.COREPACK_ENABLE_NETWORK = `0`; + await expect(runCli(cwd, [`install`, `-g`, `yarn@1.22.4`])).resolves.toEqual({ + exitCode: 0, + stdout: `Installing yarn@1.22.4...\n`, + stderr: ``, + }); + await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toEqual({ + exitCode: 0, + stdout: `1.22.4\n`, + stderr: ``, + }); + }); + }); + + it(`should report installation when changing the active hash reference`, async () => { + await xfs.mktempPromise(async cwd => { + const version = `1.22.4+sha224.0d6eecaf4d82ec12566fdd97143794d0f0c317e0d652bd4d1b305430`; + await expect(runCli(cwd, [`install`, `-g`, `yarn@${version}`])).resolves.toEqual({ + exitCode: 0, + stdout: `Installing yarn@${version}...\n`, + stderr: ``, + }); + + process.env.COREPACK_ENABLE_NETWORK = `0`; + await expect(runCli(cwd, [`install`, `-g`, `yarn@1.22.4`])).resolves.toEqual({ + exitCode: 0, + stdout: `Installing yarn@1.22.4...\n`, + stderr: ``, + }); + await expect(xfs.readJsonPromise(ppath.join(npath.toPortablePath(process.env.COREPACK_HOME!), `lastKnownGood.json`))).resolves.toEqual({ + yarn: `1.22.4`, + }); + }); + }); + + it(`should report installation when the globally active version is missing from the cache`, async () => { + const home = npath.toPortablePath(process.env.COREPACK_HOME!); + await xfs.writeJsonPromise(ppath.join(home, `lastKnownGood.json`), {yarn: `1.22.4`}); + + await xfs.mktempPromise(async cwd => { + await expect(runCli(cwd, [`install`, `-g`, `yarn@1.22.4`])).resolves.toEqual({ + exitCode: 0, + stdout: `Installing yarn@1.22.4...\n`, + stderr: ``, + }); + + process.env.COREPACK_ENABLE_NETWORK = `0`; + await expect(runCli(cwd, [`install`, `-g`, `yarn@1.22.4`])).resolves.toEqual({ + exitCode: 0, + stdout: `yarn@1.22.4 is the current version\n`, + stderr: ``, + }); + }); + }); + + it(`should report installation when downloading also updates the default version`, async () => { + process.env.COREPACK_DEFAULT_TO_LATEST = `1`; + await xfs.writeJsonPromise(ppath.join(npath.toPortablePath(process.env.COREPACK_HOME!), `lastKnownGood.json`), {yarn: `1.0.0`}); + + await xfs.mktempPromise(async cwd => { + await expect(runCli(cwd, [`install`, `-g`, `yarn@1.22.4`])).resolves.toEqual({ + exitCode: 0, + stdout: `Installing yarn@1.22.4...\n`, + stderr: ``, + }); + }); + }); + + it(`should preserve cache-only output without activating a cached version`, async () => { + await xfs.mktempPromise(async cwd => { + await expect(runCli(cwd, [`install`, `-g`, `yarn@1.0.0`])).resolves.toEqual({ + exitCode: 0, + stdout: `Installing yarn@1.0.0...\n`, + stderr: ``, + }); + + for (let attempt = 0; attempt < 2; attempt++) { + await expect(runCli(cwd, [`install`, `-g`, `--cache-only`, `yarn@1.22.4`])).resolves.toEqual({ + exitCode: 0, + stdout: `Adding yarn@1.22.4 to the cache...\n`, + stderr: ``, + }); + process.env.COREPACK_ENABLE_NETWORK = `0`; + } + + await expect(runCli(cwd, [`install`, `-g`, `--cache-only`, `yarn@1.0.0`])).resolves.toEqual({ + exitCode: 0, + stdout: `Adding yarn@1.0.0 to the cache...\n`, + stderr: ``, + }); + await expect(xfs.readJsonPromise(ppath.join(npath.toPortablePath(process.env.COREPACK_HOME!), `lastKnownGood.json`))).resolves.toEqual({ + yarn: `1.0.0`, + }); + }); + }); +});