diff --git a/sources/Engine.ts b/sources/Engine.ts index c49c1966..82c1513a 100644 --- a/sources/Engine.ts +++ b/sources/Engine.ts @@ -199,7 +199,6 @@ export class Engine { const packageManagerInfo = await corepackUtils.installVersion(folderUtils.getInstallFolder(), locator, { spec, }); - spec.bin ??= packageManagerInfo.bin; return { ...packageManagerInfo, diff --git a/sources/corepackUtils.ts b/sources/corepackUtils.ts index 738a9f77..db040bc0 100644 --- a/sources/corepackUtils.ts +++ b/sources/corepackUtils.ts @@ -16,6 +16,7 @@ import * as httpUtils from './httpUtils import * as nodeUtils from './nodeUtils'; import * as npmRegistryUtils from './npmRegistryUtils'; import {RegistrySpec, Descriptor, Locator, PackageManagerSpec} from './types'; +import {BinList, BinSpec, InstallSpec} from './types'; export function getRegistryFromPackageManagerSpec(spec: PackageManagerSpec) { return process.env.COREPACK_NPM_REGISTRY @@ -124,7 +125,15 @@ function parseURLReference(locator: Locator) { return {version: encodeURIComponent(href), build: []}; } -export async function installVersion(installTarget: string, locator: Locator, {spec}: {spec: PackageManagerSpec}) { +function isValidBinList(x: unknown): x is BinList { + return Array.isArray(x) && x.length > 0; +} + +function isValidBinSpec(x: unknown): x is BinSpec { + return typeof x === `object` && x !== null && !Array.isArray(x) && Object.keys(x).length > 0; +} + +export async function installVersion(installTarget: string, locator: Locator, {spec}: {spec: PackageManagerSpec}): Promise { const locatorIsASupportedPackageManager = isSupportedPackageManagerLocator(locator); const locatorReference = locatorIsASupportedPackageManager ? semver.parse(locator.reference)! : parseURLReference(locator); const {version, build} = locatorReference; @@ -152,13 +161,18 @@ export async function installVersion(installTarget: string, locator: Locator, {s let url: string; if (locatorIsASupportedPackageManager) { - const defaultNpmRegistryURL = spec.url.replace(`{}`, version); - url = process.env.COREPACK_NPM_REGISTRY ? - defaultNpmRegistryURL.replace( - npmRegistryUtils.DEFAULT_NPM_REGISTRY_URL, - () => process.env.COREPACK_NPM_REGISTRY!, - ) : - defaultNpmRegistryURL; + url = spec.url.replace(`{}`, version); + if (process.env.COREPACK_NPM_REGISTRY) { + const registry = getRegistryFromPackageManagerSpec(spec); + if (registry.type === `npm`) { + url = await npmRegistryUtils.fetchTarballUrl(registry.package, version); + } else { + url = url.replace( + npmRegistryUtils.DEFAULT_NPM_REGISTRY_URL, + () => process.env.COREPACK_NPM_REGISTRY!, + ); + } + } } else { url = decodeURIComponent(version); } @@ -191,13 +205,34 @@ export async function installVersion(installTarget: string, locator: Locator, {s const hash = stream.pipe(createHash(algo)); await once(sendTo, `finish`); - let bin; - if (!locatorIsASupportedPackageManager) { - if (ext === `.tgz`) { - bin = require(path.join(tmpFolder, `package.json`)).bin; - } else if (ext === `.js`) { + let bin: BinSpec | BinList; + const isSingleFile = outputFile !== null; + + // In config, yarn berry is expected to be downloaded as a single file, + // and therefore `spec.bin` is an array. However, when dowloaded from + // custom npm registry as tarball, `bin` should be a map. + // In this case, we ignore the configured `spec.bin`. + + if (isSingleFile) { + if (locatorIsASupportedPackageManager && isValidBinList(spec.bin)) { + bin = spec.bin; + } else { bin = [locator.name]; } + } else { + if (locatorIsASupportedPackageManager && isValidBinSpec(spec.bin)) { + bin = spec.bin; + } else { + const {name: packageName, bin: packageBin} = require(path.join(tmpFolder, `package.json`)); + if (typeof packageBin === `string`) { + // When `bin` is a string, the name of the executable is the name of the package. + bin = {[packageName]: packageBin}; + } else if (isValidBinSpec(packageBin)) { + bin = packageBin; + } else { + throw new Error(`Unable to locate bin in package.json`); + } + } } const actualHash = hash.digest(`hex`); @@ -292,10 +327,11 @@ async function renameUnderWindows(oldPath: fs.PathLike, newPath: fs.PathLike) { /** * Loads the binary, taking control of the current process. */ -export async function runVersion(locator: Locator, installSpec: { location: string, spec: PackageManagerSpec }, binName: string, args: Array): Promise { +export async function runVersion(locator: Locator, installSpec: InstallSpec & {spec: PackageManagerSpec}, binName: string, args: Array): Promise { let binPath: string | null = null; - if (Array.isArray(installSpec.spec.bin)) { - if (installSpec.spec.bin.some(bin => bin === binName)) { + const bin = installSpec.bin ?? installSpec.spec.bin; + if (Array.isArray(bin)) { + if (bin.some(name => name === binName)) { const parsedUrl = new URL(installSpec.spec.url); const ext = path.posix.extname(parsedUrl.pathname); if (ext === `.js`) { @@ -303,7 +339,7 @@ export async function runVersion(locator: Locator, installSpec: { location: stri } } } else { - for (const [name, dest] of Object.entries(installSpec.spec.bin)) { + for (const [name, dest] of Object.entries(bin)) { if (name === binName) { binPath = path.join(installSpec.location, dest); break; diff --git a/sources/npmRegistryUtils.ts b/sources/npmRegistryUtils.ts index a3516776..1d71c04a 100644 --- a/sources/npmRegistryUtils.ts +++ b/sources/npmRegistryUtils.ts @@ -48,3 +48,16 @@ export async function fetchAvailableVersions(packageName: string) { const metadata = await fetchAsJson(packageName); return Object.keys(metadata.versions); } + +export async function fetchTarballUrl(packageName: string, version: string) { + const metadata = await fetchAsJson(packageName); + const versionMetadata = metadata.versions?.[version]; + if (versionMetadata === undefined) + throw new Error(`${packageName}@${version} does not exist.`); + + const {tarball} = versionMetadata.dist; + if (tarball === undefined || !tarball.startsWith(`http`)) + throw new Error(`${packageName}@${version} does not have a valid tarball.`); + + return tarball; +} diff --git a/sources/types.ts b/sources/types.ts index 0a48e86f..20d66233 100644 --- a/sources/types.ts +++ b/sources/types.ts @@ -53,6 +53,12 @@ export interface PackageManagerSpec { }; } +export interface InstallSpec { + location: string; + bin?: BinList | BinSpec; + hash: string; +} + /** * The data structure found in config.json */ diff --git a/tests/main.test.ts b/tests/main.test.ts index 9bd409c0..e006131f 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -758,3 +758,46 @@ it(`should be able to show the latest version`, async () => { }); }); }); + +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 expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stdout: /^1\.\d+\.\d+\r?\n$/, + stderr: /^Corepack is about to download https:\/\/registry\.npmmirror\.com\/yarn\/-\/yarn-1\.\d+\.\d+\.tgz\r?\n$/, + }); + + // Should keep working with cache + await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stdout: /^1\.\d+\.\d+\r?\n$/, + stderr: ``, + }); + }); +}); + +it(`should download yarn berry 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` as Filename), { + packageManager: `yarn@3.0.0`, + }); + + await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stdout: `3.0.0\n`, + stderr: `Corepack is about to download https://registry.npmmirror.com/@yarnpkg/cli-dist/-/cli-dist-3.0.0.tgz\n`, + }); + + // Should keep working with cache + await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ + exitCode: 0, + stdout: `3.0.0\n`, + stderr: ``, + }); + }); +}); diff --git a/tests/nocks.db b/tests/nocks.db index 47607f15..4c88f940 100644 Binary files a/tests/nocks.db and b/tests/nocks.db differ