From 1daffab60089e1218f346c32004685efe7221a44 Mon Sep 17 00:00:00 2001 From: Aschenbath Date: Tue, 20 Oct 2026 14:33:34 +0800 Subject: [PATCH] fix: route Yarn registry tarballs through the configured npm mirror --- README.md | 4 +++- sources/corepackUtils.ts | 12 ++---------- sources/npmRegistryUtils.ts | 12 ++++++++++++ tests/_registryServer.mjs | 2 +- tests/main.test.ts | 7 ++++--- tests/npmRegistryUtils.test.ts | 36 ++++++++++++++++++++++++++++++++++ 6 files changed, 58 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index e2adeb949..2a0550b7d 100644 --- a/README.md +++ b/README.md @@ -349,7 +349,9 @@ same major line. Should you need to upgrade to a new major, use an explicit (useful for commands like `yarn init`). - `COREPACK_NPM_REGISTRY` sets the registry base url used when retrieving - package managers from npm. Default value is `https://registry.npmjs.org` + package managers from npm. Default value is `https://registry.npmjs.org`. + Tarball URLs hosted on `registry.npmjs.org` or `registry.yarnpkg.com` are + redirected to this registry, including Yarn Classic downloads. - `COREPACK_NPM_TOKEN` sets a Bearer token authorization header when connecting to a npm type registry. diff --git a/sources/corepackUtils.ts b/sources/corepackUtils.ts index d38df1c7b..d4c495204 100644 --- a/sources/corepackUtils.ts +++ b/sources/corepackUtils.ts @@ -269,19 +269,11 @@ export async function installVersion(installTarget: string, locator: Locator, {s binPath = registry.bin; } } - url = url.replace( - npmRegistryUtils.DEFAULT_NPM_REGISTRY_URL, - () => process.env.COREPACK_NPM_REGISTRY!, - ); + url = npmRegistryUtils.overrideRegistryURL(url); } } else { url = decodeURIComponent(version); - if (process.env.COREPACK_NPM_REGISTRY && url.startsWith(npmRegistryUtils.DEFAULT_NPM_REGISTRY_URL)) { - url = url.replace( - npmRegistryUtils.DEFAULT_NPM_REGISTRY_URL, - () => process.env.COREPACK_NPM_REGISTRY!, - ); - } + url = npmRegistryUtils.overrideRegistryURL(url); } debugUtils.log(`Installing ${locator.name}@${version} from ${url}`); diff --git a/sources/npmRegistryUtils.ts b/sources/npmRegistryUtils.ts index 706046be2..47b42bb44 100644 --- a/sources/npmRegistryUtils.ts +++ b/sources/npmRegistryUtils.ts @@ -13,6 +13,18 @@ export const DEFAULT_HEADERS: Record = { }; export const DEFAULT_NPM_REGISTRY_URL = `https://registry.npmjs.org`; +export function overrideRegistryURL(url: string) { + if (!process.env.COREPACK_NPM_REGISTRY) + return url; + + const parsedUrl = new URL(url); + if (parsedUrl.origin !== DEFAULT_NPM_REGISTRY_URL && parsedUrl.origin !== `https://registry.yarnpkg.com`) + return url; + + const registry = process.env.COREPACK_NPM_REGISTRY.replace(/\/+$/, ``); + return `${registry}${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`; +} + export async function fetchAsJson(packageName: string, version?: string) { // Strip any trailing slashes so a `COREPACK_NPM_REGISTRY` with a trailing // slash does not produce a double slash in the request URL (some registries, diff --git a/tests/_registryServer.mjs b/tests/_registryServer.mjs index cb4553d04..65594713a 100644 --- a/tests/_registryServer.mjs +++ b/tests/_registryServer.mjs @@ -102,7 +102,7 @@ function generateVersionMetadata(packageName, version) { dist: { shasum, size: mockPackageTarGz.length, - tarball: `https://registry.npmjs.org/${packageName}/-/${packageName}-${version}.tgz`, + tarball: `${process.env.TEST_TARBALL_REGISTRY || `https://registry.npmjs.org`}/${packageName}/-/${packageName}-${version}.tgz`, ...generateSignature(packageName, version), }, }; diff --git a/tests/main.test.ts b/tests/main.test.ts index 4bb7f40f4..8655a35f7 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -1883,7 +1883,8 @@ for (const authType of [`COREPACK_NPM_REGISTRY`, `COREPACK_NPM_TOKEN`, `COREPACK process.env.COREPACK_INTEGRITY_KEYS = ``; }); - it(`should download yarn classic`, async () => { + it.each([`https://registry.npmjs.org`, `https://registry.yarnpkg.com`])(`should download yarn classic with tarballs on %s`, async registry => { + process.env.TEST_TARBALL_REGISTRY = registry; await xfs.mktempPromise(async cwd => { await expect(runCli(cwd, [`yarn@1.x`, `--version`], true)).resolves.toMatchObject({ exitCode: 0, @@ -1917,9 +1918,9 @@ for (const authType of [`COREPACK_NPM_REGISTRY`, `COREPACK_NPM_TOKEN`, `COREPACK }); }); - it(`should download custom package manager`, async () => { + it.each([`https://registry.npmjs.org`, `https://registry.yarnpkg.com`])(`should download custom package manager from %s`, async registry => { await xfs.mktempPromise(async cwd => { - await expect(runCli(cwd, [`customPkgManager@https://registry.npmjs.org/customPkgManager/-/customPkgManager-1.0.0.tgz`, `--version`], true)).resolves.toMatchObject({ + await expect(runCli(cwd, [`customPkgManager@${registry}/customPkgManager/-/customPkgManager-1.0.0.tgz`, `--version`], true)).resolves.toMatchObject({ exitCode: 0, stdout: `customPkgManager: Hello from custom registry\n`, stderr: ``, diff --git a/tests/npmRegistryUtils.test.ts b/tests/npmRegistryUtils.test.ts index b9d9cb9cf..2df2d3c4a 100644 --- a/tests/npmRegistryUtils.test.ts +++ b/tests/npmRegistryUtils.test.ts @@ -6,9 +6,45 @@ import {describe, beforeEach, it, expect, vi} from 'vitest'; import {fetchAsJson as httpFetchAsJson} from '../sources/httpUtils'; import {DEFAULT_HEADERS, DEFAULT_NPM_REGISTRY_URL} from '../sources/npmRegistryUtils'; import {fetchLatestStableVersion, fetchAsJson, verifySignature} from '../sources/npmRegistryUtils'; +import {overrideRegistryURL} from '../sources/npmRegistryUtils'; vi.mock(`../sources/httpUtils.ts`); +describe(`npm registry utils overrideRegistryURL`, () => { + it.each([`https://registry.npmjs.org`, `https://registry.yarnpkg.com`])(`rewrites tarballs on %s to the configured registry`, registry => { + process.env.COREPACK_NPM_REGISTRY = `https://mirror.example.org/npm///`; + + expect(overrideRegistryURL(`${registry}/yarn/-/yarn-1.22.22.tgz?download=1#sha224.abc`)) + .toBe(`https://mirror.example.org/npm/yarn/-/yarn-1.22.22.tgz?download=1#sha224.abc`); + }); + + it(`preserves registry credentials and scoped package paths`, () => { + process.env.COREPACK_NPM_REGISTRY = `https://user:pass@mirror.example.org/npm/`; + + expect(overrideRegistryURL(`https://registry.yarnpkg.com/@yarnpkg/cli-dist/-/cli-dist-4.0.0.tgz`)) + .toBe(`https://user:pass@mirror.example.org/npm/@yarnpkg/cli-dist/-/cli-dist-4.0.0.tgz`); + }); + + it.each([ + `https://registry.yarnpkg.com/yarn/-/yarn-1.22.22.tgz`, + `https://registry.npmjs.org/pnpm/-/pnpm-10.0.0.tgz`, + ])(`preserves %s when no custom registry is configured`, url => { + expect(overrideRegistryURL(url)).toBe(url); + }); + + it.each([ + `https://repo.yarnpkg.com/4.0.0/packages/yarnpkg-cli/bin/yarn.js`, + `https://cdn.example.org/yarn.tgz?registry=https://registry.yarnpkg.com`, + `https://registry.yarnpkg.com.example.org/yarn.tgz`, + `https://registry.npmjs.org.example.org/yarn.tgz`, + `https://example.org/https://registry.npmjs.org/yarn.tgz`, + ])(`preserves URLs from other origins: %s`, url => { + process.env.COREPACK_NPM_REGISTRY = `https://mirror.example.org`; + + expect(overrideRegistryURL(url)).toBe(url); + }); +}); + describe(`npm registry utils fetchAsJson`, () => { beforeEach(() => { vi.resetAllMocks();