Skip to content

Commit 47be27c

Browse files
authored
fix: improve support for COREPACK_NPM_REGISTRY with Yarn Berry (#396)
1 parent 88d504c commit 47be27c

File tree

6 files changed

+115
-18
lines changed

6 files changed

+115
-18
lines changed

sources/Engine.ts

-1
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,6 @@ export class Engine {
199199
const packageManagerInfo = await corepackUtils.installVersion(folderUtils.getInstallFolder(), locator, {
200200
spec,
201201
});
202-
spec.bin ??= packageManagerInfo.bin;
203202

204203
return {
205204
...packageManagerInfo,

sources/corepackUtils.ts

+53-17
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import * as httpUtils from './httpUtils
1616
import * as nodeUtils from './nodeUtils';
1717
import * as npmRegistryUtils from './npmRegistryUtils';
1818
import {RegistrySpec, Descriptor, Locator, PackageManagerSpec} from './types';
19+
import {BinList, BinSpec, InstallSpec} from './types';
1920

2021
export function getRegistryFromPackageManagerSpec(spec: PackageManagerSpec) {
2122
return process.env.COREPACK_NPM_REGISTRY
@@ -124,7 +125,15 @@ function parseURLReference(locator: Locator) {
124125
return {version: encodeURIComponent(href), build: []};
125126
}
126127

127-
export async function installVersion(installTarget: string, locator: Locator, {spec}: {spec: PackageManagerSpec}) {
128+
function isValidBinList(x: unknown): x is BinList {
129+
return Array.isArray(x) && x.length > 0;
130+
}
131+
132+
function isValidBinSpec(x: unknown): x is BinSpec {
133+
return typeof x === `object` && x !== null && !Array.isArray(x) && Object.keys(x).length > 0;
134+
}
135+
136+
export async function installVersion(installTarget: string, locator: Locator, {spec}: {spec: PackageManagerSpec}): Promise<InstallSpec> {
128137
const locatorIsASupportedPackageManager = isSupportedPackageManagerLocator(locator);
129138
const locatorReference = locatorIsASupportedPackageManager ? semver.parse(locator.reference)! : parseURLReference(locator);
130139
const {version, build} = locatorReference;
@@ -152,13 +161,18 @@ export async function installVersion(installTarget: string, locator: Locator, {s
152161

153162
let url: string;
154163
if (locatorIsASupportedPackageManager) {
155-
const defaultNpmRegistryURL = spec.url.replace(`{}`, version);
156-
url = process.env.COREPACK_NPM_REGISTRY ?
157-
defaultNpmRegistryURL.replace(
158-
npmRegistryUtils.DEFAULT_NPM_REGISTRY_URL,
159-
() => process.env.COREPACK_NPM_REGISTRY!,
160-
) :
161-
defaultNpmRegistryURL;
164+
url = spec.url.replace(`{}`, version);
165+
if (process.env.COREPACK_NPM_REGISTRY) {
166+
const registry = getRegistryFromPackageManagerSpec(spec);
167+
if (registry.type === `npm`) {
168+
url = await npmRegistryUtils.fetchTarballUrl(registry.package, version);
169+
} else {
170+
url = url.replace(
171+
npmRegistryUtils.DEFAULT_NPM_REGISTRY_URL,
172+
() => process.env.COREPACK_NPM_REGISTRY!,
173+
);
174+
}
175+
}
162176
} else {
163177
url = decodeURIComponent(version);
164178
}
@@ -191,13 +205,34 @@ export async function installVersion(installTarget: string, locator: Locator, {s
191205
const hash = stream.pipe(createHash(algo));
192206
await once(sendTo, `finish`);
193207

194-
let bin;
195-
if (!locatorIsASupportedPackageManager) {
196-
if (ext === `.tgz`) {
197-
bin = require(path.join(tmpFolder, `package.json`)).bin;
198-
} else if (ext === `.js`) {
208+
let bin: BinSpec | BinList;
209+
const isSingleFile = outputFile !== null;
210+
211+
// In config, yarn berry is expected to be downloaded as a single file,
212+
// and therefore `spec.bin` is an array. However, when dowloaded from
213+
// custom npm registry as tarball, `bin` should be a map.
214+
// In this case, we ignore the configured `spec.bin`.
215+
216+
if (isSingleFile) {
217+
if (locatorIsASupportedPackageManager && isValidBinList(spec.bin)) {
218+
bin = spec.bin;
219+
} else {
199220
bin = [locator.name];
200221
}
222+
} else {
223+
if (locatorIsASupportedPackageManager && isValidBinSpec(spec.bin)) {
224+
bin = spec.bin;
225+
} else {
226+
const {name: packageName, bin: packageBin} = require(path.join(tmpFolder, `package.json`));
227+
if (typeof packageBin === `string`) {
228+
// When `bin` is a string, the name of the executable is the name of the package.
229+
bin = {[packageName]: packageBin};
230+
} else if (isValidBinSpec(packageBin)) {
231+
bin = packageBin;
232+
} else {
233+
throw new Error(`Unable to locate bin in package.json`);
234+
}
235+
}
201236
}
202237

203238
const actualHash = hash.digest(`hex`);
@@ -292,18 +327,19 @@ async function renameUnderWindows(oldPath: fs.PathLike, newPath: fs.PathLike) {
292327
/**
293328
* Loads the binary, taking control of the current process.
294329
*/
295-
export async function runVersion(locator: Locator, installSpec: { location: string, spec: PackageManagerSpec }, binName: string, args: Array<string>): Promise<void> {
330+
export async function runVersion(locator: Locator, installSpec: InstallSpec & {spec: PackageManagerSpec}, binName: string, args: Array<string>): Promise<void> {
296331
let binPath: string | null = null;
297-
if (Array.isArray(installSpec.spec.bin)) {
298-
if (installSpec.spec.bin.some(bin => bin === binName)) {
332+
const bin = installSpec.bin ?? installSpec.spec.bin;
333+
if (Array.isArray(bin)) {
334+
if (bin.some(name => name === binName)) {
299335
const parsedUrl = new URL(installSpec.spec.url);
300336
const ext = path.posix.extname(parsedUrl.pathname);
301337
if (ext === `.js`) {
302338
binPath = path.join(installSpec.location, path.posix.basename(parsedUrl.pathname));
303339
}
304340
}
305341
} else {
306-
for (const [name, dest] of Object.entries(installSpec.spec.bin)) {
342+
for (const [name, dest] of Object.entries(bin)) {
307343
if (name === binName) {
308344
binPath = path.join(installSpec.location, dest);
309345
break;

sources/npmRegistryUtils.ts

+13
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,16 @@ export async function fetchAvailableVersions(packageName: string) {
4848
const metadata = await fetchAsJson(packageName);
4949
return Object.keys(metadata.versions);
5050
}
51+
52+
export async function fetchTarballUrl(packageName: string, version: string) {
53+
const metadata = await fetchAsJson(packageName);
54+
const versionMetadata = metadata.versions?.[version];
55+
if (versionMetadata === undefined)
56+
throw new Error(`${packageName}@${version} does not exist.`);
57+
58+
const {tarball} = versionMetadata.dist;
59+
if (tarball === undefined || !tarball.startsWith(`http`))
60+
throw new Error(`${packageName}@${version} does not have a valid tarball.`);
61+
62+
return tarball;
63+
}

sources/types.ts

+6
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ export interface PackageManagerSpec {
5353
};
5454
}
5555

56+
export interface InstallSpec {
57+
location: string;
58+
bin?: BinList | BinSpec;
59+
hash: string;
60+
}
61+
5662
/**
5763
* The data structure found in config.json
5864
*/

tests/main.test.ts

+43
Original file line numberDiff line numberDiff line change
@@ -758,3 +758,46 @@ it(`should be able to show the latest version`, async () => {
758758
});
759759
});
760760
});
761+
762+
it(`should download yarn classic from custom registry`, async () => {
763+
await xfs.mktempPromise(async cwd => {
764+
process.env.COREPACK_NPM_REGISTRY = `https://registry.npmmirror.com`;
765+
process.env.COREPACK_ENABLE_DOWNLOAD_PROMPT = `1`;
766+
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
767+
exitCode: 0,
768+
stdout: /^1\.\d+\.\d+\r?\n$/,
769+
stderr: /^Corepack is about to download https:\/\/registry\.npmmirror\.com\/yarn\/-\/yarn-1\.\d+\.\d+\.tgz\r?\n$/,
770+
});
771+
772+
// Should keep working with cache
773+
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
774+
exitCode: 0,
775+
stdout: /^1\.\d+\.\d+\r?\n$/,
776+
stderr: ``,
777+
});
778+
});
779+
});
780+
781+
it(`should download yarn berry from custom registry`, async () => {
782+
await xfs.mktempPromise(async cwd => {
783+
process.env.COREPACK_NPM_REGISTRY = `https://registry.npmmirror.com`;
784+
process.env.COREPACK_ENABLE_DOWNLOAD_PROMPT = `1`;
785+
786+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
787+
packageManager: `yarn@3.0.0`,
788+
});
789+
790+
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
791+
exitCode: 0,
792+
stdout: `3.0.0\n`,
793+
stderr: `Corepack is about to download https://registry.npmmirror.com/@yarnpkg/cli-dist/-/cli-dist-3.0.0.tgz\n`,
794+
});
795+
796+
// Should keep working with cache
797+
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
798+
exitCode: 0,
799+
stdout: `3.0.0\n`,
800+
stderr: ``,
801+
});
802+
});
803+
});

tests/nocks.db

2.16 MB
Binary file not shown.

0 commit comments

Comments
 (0)