Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Download yarn from custom npm registry #396

Merged
merged 10 commits into from
Feb 27, 2024
1 change: 0 additions & 1 deletion sources/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,6 @@ export class Engine {
const packageManagerInfo = await corepackUtils.installVersion(folderUtils.getInstallFolder(), locator, {
spec,
});
spec.bin ??= packageManagerInfo.bin;

return {
...packageManagerInfo,
Expand Down
70 changes: 53 additions & 17 deletions sources/corepackUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,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
Expand Down Expand Up @@ -123,7 +124,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<InstallSpec> {
const locatorIsASupportedPackageManager = isSupportedPackageManagerLocator(locator);
const locatorReference = locatorIsASupportedPackageManager ? semver.parse(locator.reference)! : parseURLReference(locator);
const {version, build} = locatorReference;
Expand Down Expand Up @@ -151,13 +160,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);
}
Expand Down Expand Up @@ -190,13 +204,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`);
Expand Down Expand Up @@ -263,18 +298,19 @@ export async function installVersion(installTarget: string, locator: Locator, {s
/**
* Loads the binary, taking control of the current process.
*/
export async function runVersion(locator: Locator, installSpec: { location: string, spec: PackageManagerSpec }, binName: string, args: Array<string>): Promise<void> {
export async function runVersion(locator: Locator, installSpec: InstallSpec & {spec: PackageManagerSpec}, binName: string, args: Array<string>): Promise<void> {
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`) {
binPath = path.join(installSpec.location, path.posix.basename(parsedUrl.pathname));
}
}
} 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;
Expand Down
13 changes: 13 additions & 0 deletions sources/npmRegistryUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
6 changes: 6 additions & 0 deletions sources/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ export interface PackageManagerSpec {
};
}

export interface InstallSpec {
location: string;
bin?: BinList | BinSpec;
hash: string;
}

/**
* The data structure found in config.json
*/
Expand Down
43 changes: 43 additions & 0 deletions tests/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: `[email protected]`,
});

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: ``,
});
});
});
Binary file modified tests/nocks.db
Binary file not shown.