Skip to content
Closed
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
3 changes: 1 addition & 2 deletions .github/workflows/sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ jobs:

LATEST_NPM=$(curl https://registry.npmjs.org/npm | jq '.["dist-tags"].latest + "+sha1." + .versions[.["dist-tags"].latest].dist.shasum')
LATEST_PNPM=$(curl https://registry.npmjs.org/pnpm | jq '.["dist-tags"].latest + "+sha1." + .versions[.["dist-tags"].latest].dist.shasum')
LATEST_YARN=$(curl https://registry.npmjs.org/yarn | jq '.["dist-tags"].latest + "+sha1." + .versions[.["dist-tags"].latest].dist.shasum')
LATEST_BERRY=$(jq -n '$version + "+sha224." + $checksum' --arg version "$LATEST_BERRY_VERSION" --arg checksum "$(curl https://repo.yarnpkg.com/"$LATEST_BERRY_VERSION"/packages/yarnpkg-cli/bin/yarn.js | openssl dgst -sha224 | cut -d' ' -f2)")

git --no-pager show HEAD:config.json | jq '. * '"{
Expand All @@ -41,7 +40,7 @@ jobs:
default: $LATEST_PNPM,
},
yarn: {
default: $LATEST_YARN,
default: $LATEST_BERRY,
transparent: {
default: $LATEST_BERRY,
},
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,10 @@ When running Corepack within projects that don't list a supported package
manager, it will default to a set of Known Good Releases.

If there is no Known Good Release for the requested package manager, Corepack
looks up the npm registry for the latest available version and cache it for
future use.
looks up the package manager's registry for the latest available version and
caches it for future use. Yarn defaults to Yarn Modern. Projects that pin Yarn
Classic, or users who select it with `corepack install -g yarn@1`, continue to
use Yarn Classic.

The Known Good Releases can be updated system-wide using `corepack install -g`.
When Corepack downloads a new version of a given package manager on the same
Expand Down
10 changes: 7 additions & 3 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,14 @@
}
},
"yarn": {
"default": "1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610",
"default": "4.18.0+sha224.5707fce90df5d8720fae4e85a07ab55e90aa20fded8914893e2ba225",
"fetchLatestFrom": {
"type": "npm",
"package": "yarn"
"type": "url",
"url": "https://repo.yarnpkg.com/tags",
"fields": {
"tags": "aliases",
"versions": "tags"
}
},
"transparent": {
"default": "4.18.0+sha224.5707fce90df5d8720fae4e85a07ab55e90aa20fded8914893e2ba225",
Expand Down
5 changes: 4 additions & 1 deletion sources/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,10 @@ export class Engine {
return definition.default;
}

const reference = await corepackUtils.fetchLatestStableVersion(definition.fetchLatestFrom);
const registry = process.env.COREPACK_NPM_REGISTRY
? corepackUtils.getRegistryFromPackageManagerSpec(this.getPackageManagerSpecFor({name: packageManager, reference: definition.default}))
: definition.fetchLatestFrom;
const reference = await corepackUtils.fetchLatestStableVersion(registry);
debugUtils.log(`Search for default version: found in remote registry ${packageManager}@${reference}`);

try {
Expand Down
23 changes: 20 additions & 3 deletions sources/corepackUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ export function getRegistryFromPackageManagerSpec(spec: PackageManagerSpec) {
export async function fetchLatestStableVersion(spec: RegistrySpec): Promise<string> {
switch (spec.type) {
case `npm`: {
return await npmRegistryUtils.fetchLatestStableVersion(spec.package);
const reference = await npmRegistryUtils.fetchLatestStableVersion(spec.package);
// A registry tarball's hash cannot be used to verify a single file
// extracted from it (such as Yarn Modern's bin/yarn.js).
return spec.bin ? reference.replace(/\+.*/, ``) : reference;
}
case `url`: {
const data = await httpUtils.fetchAsJson(spec.url);
Expand Down Expand Up @@ -140,7 +143,7 @@ function isValidBinSpec(x: unknown): x is BinSpec {
return typeof x === `object` && x !== null && !Array.isArray(x) && Object.keys(x).length > 0;
}

async function download(installTarget: string, url: string, algo: string, binPath: string | null = null): Promise<DownloadSpec> {
async function download(installTarget: string, url: string, algo: string, binPath: string | null = null, expectedTarballHash: string | null = null): Promise<DownloadSpec> {
// Creating a temporary folder inside the install folder means that we
// are sure it'll be in the same drive as the destination, so we can
// just move it there atomically once we are done
Expand Down Expand Up @@ -173,8 +176,17 @@ async function download(installTarget: string, url: string, algo: string, binPat
stream.pipe(sendTo);

let hash = !binPath ? stream.pipe(createHash(algo)) : null;
const tarballHash = expectedTarballHash ? stream.pipe(createHash(`sha512`)) : null;
await once(sendTo, `finish`);

if (tarballHash) {
const actualTarballHash = tarballHash.digest(`hex`);
if (actualTarballHash !== expectedTarballHash) {
await fs.promises.rm(tmpFolder, {recursive: true, force: true});
throw new Error(`Mismatch hashes. Expected ${expectedTarballHash}, got ${actualTarballHash}`);
}
}

if (binPath) {
const downloadedBin = path.join(tmpFolder, binPath);
outputFile = path.join(tmpFolder, path.basename(downloadedBin));
Expand Down Expand Up @@ -286,7 +298,12 @@ export async function installVersion(installTarget: string, locator: Locator, {s

debugUtils.log(`Installing ${locator.name}@${version} from ${url}`);
const algo = build[0] ?? `sha512`;
const {tmpFolder, outputFile, hash: actualHash} = await download(installTarget, url, algo, binPath);
// npm metadata describes the tarball, while the locator hash describes
// the extracted binary. Verify both when downloading a single-file manager.
const expectedTarballHash = binPath && integrity!
? Buffer.from(integrity.slice(`sha512-`.length), `base64`).toString(`hex`)
: null;
const {tmpFolder, outputFile, hash: actualHash} = await download(installTarget, url, algo, binPath, expectedTarballHash);

let bin: BinSpec | BinList;
const isSingleFile = outputFile !== null;
Expand Down
63 changes: 63 additions & 0 deletions tests/Engine.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {xfs, npath} from '@yarnpkg/fslib';
import {beforeEach, describe, expect, it, vi} from 'vitest';

import config from '../config.json' with {type: 'json'};
import {Engine} from '../sources/Engine.ts';
import * as corepackUtils from '../sources/corepackUtils.ts';
import type {RegistrySpec} from '../sources/types.ts';

vi.mock(`../sources/corepackUtils.ts`);

describe(`Yarn default version`, () => {
beforeEach(async () => {
vi.resetAllMocks();
vi.mocked(corepackUtils.isSupportedPackageManagerLocator).mockReturnValue(true);
const home = await xfs.mktempPromise();
process.env.COREPACK_HOME = npath.fromPortablePath(home);
return () => xfs.removePromise(home);
});

it(`uses the bundled Modern version when latest lookups are disabled`, async () => {
process.env.COREPACK_DEFAULT_TO_LATEST = `0`;

await expect(new Engine().getDefaultVersion(`yarn`)).resolves.toBe(config.definitions.yarn.default);
expect(config.definitions.yarn.default).toMatch(/^4\./);
expect(corepackUtils.fetchLatestStableVersion).not.toHaveBeenCalled();
});

it(`looks up the Modern release index on a fresh install`, async () => {
vi.mocked(corepackUtils.fetchLatestStableVersion).mockResolvedValue(`4.99.0`);

await expect(new Engine().getDefaultVersion(`yarn`)).resolves.toBe(`4.99.0`);
expect(corepackUtils.fetchLatestStableVersion).toHaveBeenCalledWith({
type: `url`,
url: `https://repo.yarnpkg.com/tags`,
fields: {tags: `aliases`, versions: `tags`},
});
// Later calls use the saved Known Good Release, including offline calls.
process.env.COREPACK_ENABLE_NETWORK = `0`;
await expect(new Engine().getDefaultVersion(`yarn`)).resolves.toBe(`4.99.0`);
expect(corepackUtils.fetchLatestStableVersion).toHaveBeenCalledTimes(1);
});

it(`preserves an activated Classic release`, async () => {
const engine = new Engine();
await engine.activatePackageManager({name: `yarn`, reference: `1.22.22`});

await expect(engine.getDefaultVersion(`yarn`)).resolves.toBe(`1.22.22`);
expect(corepackUtils.fetchLatestStableVersion).not.toHaveBeenCalled();
});

it(`uses the Modern npm package when a custom registry is configured`, async () => {
process.env.COREPACK_NPM_REGISTRY = `https://registry.example.org`;
const registry = {
type: `npm`, package: `@yarnpkg/cli-dist`, bin: `bin/yarn.js`,
} satisfies RegistrySpec;
vi.mocked(corepackUtils.getRegistryFromPackageManagerSpec).mockReturnValue(registry);
vi.mocked(corepackUtils.fetchLatestStableVersion).mockResolvedValue(`4.99.0`);

await expect(new Engine().getDefaultVersion(`yarn`)).resolves.toBe(`4.99.0`);
expect(corepackUtils.getRegistryFromPackageManagerSpec).toHaveBeenCalledWith(config.definitions.yarn.ranges[`>=2.0.0`]);
expect(corepackUtils.fetchLatestStableVersion).toHaveBeenCalledWith(registry);
});
});
36 changes: 34 additions & 2 deletions tests/corepackUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,38 @@
import {describe, it, expect} from 'vitest';
import {beforeEach, describe, it, expect, vi} from 'vitest';

import {shouldSkipIntegrityCheck} from '../sources/corepackUtils.ts';
import {fetchLatestStableVersion, shouldSkipIntegrityCheck} from '../sources/corepackUtils.ts';
import * as httpUtils from '../sources/httpUtils.ts';
import * as npmRegistryUtils from '../sources/npmRegistryUtils.ts';

vi.mock(`../sources/httpUtils.ts`);
vi.mock(`../sources/npmRegistryUtils.ts`);

describe(`latest stable version`, () => {
beforeEach(() => {
vi.resetAllMocks();
});

it(`selects the stable alias from a URL registry`, async () => {
vi.mocked(httpUtils.fetchAsJson).mockResolvedValue({aliases: {stable: `4.99.0`, canary: `5.0.0-rc.1`}});

await expect(fetchLatestStableVersion({
type: `url`, url: `https://repo.yarnpkg.com/tags`, fields: {tags: `aliases`, versions: `tags`},
})).resolves.toBe(`4.99.0`);
});

it(`does not use a tarball hash to verify an extracted binary`, async () => {
vi.mocked(npmRegistryUtils.fetchLatestStableVersion).mockResolvedValue(`4.99.0+sha512.abcdef`);

await expect(fetchLatestStableVersion({type: `npm`, package: `@yarnpkg/cli-dist`, bin: `bin/yarn.js`})).resolves.toBe(`4.99.0`);
expect(npmRegistryUtils.fetchLatestStableVersion).toHaveBeenCalledWith(`@yarnpkg/cli-dist`);
});

it(`retains the tarball hash for package managers installed as tarballs`, async () => {
vi.mocked(npmRegistryUtils.fetchLatestStableVersion).mockResolvedValue(`12.0.0+sha512.abcdef`);

await expect(fetchLatestStableVersion({type: `npm`, package: `npm`})).resolves.toBe(`12.0.0+sha512.abcdef`);
});
});

describe(`corepack utils shouldSkipIntegrityCheck`, () => {
it(`should return false if COREPACK_INTEGRITY_KEYS env is not set`, () => {
Expand Down
46 changes: 43 additions & 3 deletions tests/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1369,20 +1369,37 @@ describe(`should show a warning on stderr before downloading when enable`, () =>
});
});

it(`should be able to show the latest version`, async () => {
it(`should offer Yarn Modern on a fresh install, including create`, async () => {
process.env.COREPACK_ENABLE_DOWNLOAD_PROMPT = `1`;
await xfs.mktempPromise(async cwd => {
await expect(runCli(cwd, [`yarn`, `create`, `--help`])).resolves.toMatchObject({
exitCode: 0,
stdout: expect.stringContaining(`yarn create`),
stderr: `! Corepack is about to download https://repo.yarnpkg.com/${config.definitions.yarn.default.split(`+`, 1)[0]}/packages/yarnpkg-cli/bin/yarn.js\n`,
});
process.env.COREPACK_ENABLE_NETWORK = `0`;
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
exitCode: 0,
stdout: `${config.definitions.yarn.default.split(`+`, 1)[0]}\n`,
stderr: ``,
});
});
});

it(`should be able to show the latest Yarn Modern version`, async () => {
process.env.COREPACK_DEFAULT_TO_LATEST = `1`;
await xfs.mktempPromise(async cwd => {
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
exitCode: 0,
stdout: /^1\.\d+\.\d+\r?\n$/,
stdout: /^(?:[4-9]|[1-9]\d+)\.\d+\.\d+\r?\n$/,
stderr: ``,
});

// Should keep working if the home folder is removed
await xfs.rmPromise(process.env.COREPACK_HOME as any, {recursive: true});
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
exitCode: 0,
stdout: /^1\.\d+\.\d+\r?\n$/,
stdout: /^(?:[4-9]|[1-9]\d+)\.\d+\.\d+\r?\n$/,
stderr: ``,
});
});
Expand All @@ -1392,6 +1409,9 @@ it(`should download yarn classic from custom registry`, async () => {
await xfs.mktempPromise(async cwd => {
process.env.COREPACK_NPM_REGISTRY = `https://registry.npmmirror.com`;
process.env.COREPACK_ENABLE_DOWNLOAD_PROMPT = `1`;
await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), {
packageManager: `[email protected]`,
});
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
exitCode: 0,
stdout: expect.stringMatching(/^1\.\d+\.\d+\r?\n$/),
Expand Down Expand Up @@ -1939,6 +1959,26 @@ describe(`handle integrity checks`, () => {
process.env.TEST_INTEGRITY = `valid`; // See `_registryServer.mjs`
});

it(`should resolve and cache Yarn Modern by default from a custom registry`, async () => {
process.env.COREPACK_DEFAULT_TO_LATEST = `1`;
await xfs.mktempPromise(async cwd => {
await expect(runCli(cwd, [`yarn`, `--version`], true)).resolves.toMatchObject({
exitCode: 0,
stdout: `yarn: Hello from custom registry\n`,
stderr: ``,
});
const lastKnownGood = await xfs.readJsonPromise(ppath.join(npath.toPortablePath(process.env.COREPACK_HOME!), `lastKnownGood.json`));
expect(lastKnownGood.yarn).toMatch(/^5\.9999\.9999(?:\+|$)/);

process.env.COREPACK_ENABLE_NETWORK = `0`;
await expect(runCli(cwd, [`yarn`, `--version`], true)).resolves.toMatchObject({
exitCode: 0,
stdout: `yarn: Hello from custom registry\n`,
stderr: ``,
});
});
});

it(`should return no error when calling 'corepack use'`, async () => {
await xfs.mktempPromise(async cwd => {
// Skip rest of the test on Windows & Node.js 18.x as it inevitably times out otherwise.
Expand Down