Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion sources/commands/Base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export abstract class BaseCommand extends Command<Context> {
throw new UsageError(`The local project doesn't feature a 'packageManager' field - please explicit the package manager to pack, or update the manifest to reference it`);
Comment thread
aduh95 marked this conversation as resolved.
Outdated

default: {
return [lookup.spec];
return [lookup.range ?? lookup.spec];
}
}
}
Expand Down
51 changes: 48 additions & 3 deletions sources/specUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {UsageError} from 'clipanion';
import fs from 'fs';
import path from 'path';
import semverSatisfies from 'semver/functions/satisfies';
import semverValid from 'semver/functions/valid';

import {PreparedPackageManagerInfo} from './Engine';
Expand Down Expand Up @@ -52,6 +53,46 @@ export function parseSpec(raw: unknown, source: string, {enforceExactVersion = t
};
}

type CorepackPackageJSON = {
packageManager?: string;
devEngines?: { packageManager?: DevEngineDependency };
};

interface DevEngineDependency {
name: string;
version: string;
}
function parsePackageJSON(packageJSONContent: CorepackPackageJSON) {
if (packageJSONContent.devEngines?.packageManager) {
const {packageManager} = packageJSONContent.devEngines;

if (Array.isArray(packageManager))
throw new UsageError(`Providing several package managers is currently not supported`);
Comment thread
aduh95 marked this conversation as resolved.
Outdated

const {version} = packageManager;
if (!version)
throw new UsageError(`Providing no version nor ranger for package manager is currently not supported`);
Comment thread
aduh95 marked this conversation as resolved.
Outdated

debugUtils.log(`devEngines defines that ${packageManager.name}@${version} is the local package manager`);

const {packageManager: pm} = packageJSONContent;
Comment thread
aduh95 marked this conversation as resolved.
Outdated
if (pm) {
if (!pm.startsWith(`${packageManager.name}@`))
throw new UsageError(`"packageManager" field is set to ${JSON.stringify(pm)} which does not match the "devEngines.packageManager" field set to ${JSON.stringify(packageManager.name)}`);

if (!semverSatisfies(pm.slice(packageManager.name.length + 1), version))
throw new UsageError(`"packageManager" field is set to ${JSON.stringify(pm)} which does not match the value defined in "devEngines.packageManager" for ${JSON.stringify(packageManager.name)} of ${JSON.stringify(version)}`);

return pm;
}


return `${packageManager.name}@${version}`;
}

return packageJSONContent.packageManager;
}

export async function setLocalPackageManager(cwd: string, info: PreparedPackageManagerInfo) {
const lookup = await loadSpec(cwd);

Expand All @@ -75,7 +116,7 @@ export async function setLocalPackageManager(cwd: string, info: PreparedPackageM
export type LoadSpecResult =
| {type: `NoProject`, target: string}
| {type: `NoSpec`, target: string}
| {type: `Found`, target: string, spec: Descriptor};
| {type: `Found`, target: string, spec: Descriptor, range?: Descriptor};

export async function loadSpec(initialCwd: string): Promise<LoadSpecResult> {
let nextCwd = initialCwd;
Expand Down Expand Up @@ -117,13 +158,17 @@ export async function loadSpec(initialCwd: string): Promise<LoadSpecResult> {
if (selection === null)
return {type: `NoProject`, target: path.join(initialCwd, `package.json`)};

const rawPmSpec = selection.data.packageManager;
const rawPmSpec = parsePackageJSON(selection.data);
if (typeof rawPmSpec === `undefined`)
return {type: `NoSpec`, target: selection.manifestPath};

debugUtils.log(`${selection.manifestPath} defines ${rawPmSpec} as local package manager`);

const spec = parseSpec(rawPmSpec, path.relative(initialCwd, selection.manifestPath));
return {
type: `Found`,
target: selection.manifestPath,
spec: parseSpec(rawPmSpec, path.relative(initialCwd, selection.manifestPath)),
spec,
range: selection.data.devEngines?.packageManager?.version && {...spec, range: selection.data.devEngines.packageManager.version},
};
}
58 changes: 44 additions & 14 deletions tests/Up.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,54 @@ beforeEach(async () => {
});

describe(`UpCommand`, () => {
it(`should upgrade the package manager from the current project`, async () => {
await xfs.mktempPromise(async cwd => {
await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), {
packageManager: `yarn@2.1.0`,
});
describe(`should update the "packageManager" field from the current project`, () => {
it(`to the same major if no devEngines range`, async () => {
await xfs.mktempPromise(async cwd => {
await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), {
packageManager: `yarn@2.1.0`,
});

await expect(runCli(cwd, [`up`])).resolves.toMatchObject({
exitCode: 0,
stderr: ``,
});
await expect(runCli(cwd, [`up`])).resolves.toMatchObject({
exitCode: 0,
stderr: ``,
});

await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toMatchObject({
packageManager: `yarn@2.4.3+sha512.8dd9fedc5451829619e526c56f42609ad88ae4776d9d3f9456d578ac085115c0c2f0fb02bb7d57fd2e1b6e1ac96efba35e80a20a056668f61c96934f67694fd0`,
await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toMatchObject({
packageManager: `yarn@2.4.3+sha512.8dd9fedc5451829619e526c56f42609ad88ae4776d9d3f9456d578ac085115c0c2f0fb02bb7d57fd2e1b6e1ac96efba35e80a20a056668f61c96934f67694fd0`,
});

await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
exitCode: 0,
stdout: `2.4.3\n`,
});
});
});

it(`to whichever range devEngines defines`, async () => {
await xfs.mktempPromise(async cwd => {
await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), {
packageManager: `yarn@1.1.0`,
devEngines: {
packageManager: {
name: `yarn`,
version: `1.x || 2.x`,
},
},
});

await expect(runCli(cwd, [`up`])).resolves.toMatchObject({
exitCode: 0,
stderr: ``,
});

await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toMatchObject({
packageManager: `yarn@2.4.3+sha512.8dd9fedc5451829619e526c56f42609ad88ae4776d9d3f9456d578ac085115c0c2f0fb02bb7d57fd2e1b6e1ac96efba35e80a20a056668f61c96934f67694fd0`,
});

await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
exitCode: 0,
stdout: `2.4.3\n`,
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
exitCode: 0,
stdout: `2.4.3\n`,
});
});
});
});
Expand Down
119 changes: 111 additions & 8 deletions tests/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,33 @@ beforeEach(async () => {
process.env.COREPACK_DEFAULT_TO_LATEST = `0`;
});

it(`should refuse to download a package manager if the hash doesn't match`, async () => {
await xfs.mktempPromise(async cwd => {
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
packageManager: `yarn@1.22.4+sha1.deadbeef`,
describe(`should refuse to download a package manager if the hash doesn't match`, () => {
it(`the one defined in "devEngines.packageManager" field`, async () => {
await xfs.mktempPromise(async cwd => {
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
devEngines: {
packageManager: {name: `yarn`, version: `1.22.4+sha1.deadbeef`},
Comment thread
aduh95 marked this conversation as resolved.
},
});

await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
exitCode: 1,
stderr: expect.stringContaining(`Mismatch hashes`),
stdout: ``,
});
});
});
it(`the one defined in "packageManager" field`, async () => {
await xfs.mktempPromise(async cwd => {
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
packageManager: `yarn@1.22.4+sha1.deadbeef`,
});

await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
exitCode: 1,
stderr: /Mismatch hashes/,
stdout: ``,
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
exitCode: 1,
stderr: expect.stringContaining(`Mismatch hashes`),
stdout: ``,
});
});
});
});
Expand Down Expand Up @@ -150,6 +167,16 @@ for (const [name, version, expectedVersion = version.split(`+`, 1)[0]] of tested
stderr: ``,
stdout: `${expectedVersion}\n`,
});

await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
devEngines: {packageManager: {name, version}},
});

await expect(runCli(cwd, [name, `--version`])).resolves.toMatchObject({
exitCode: 0,
stderr: ``,
stdout: `${expectedVersion}\n`,
});
});
});
}
Expand Down Expand Up @@ -231,6 +258,82 @@ it(`should ignore the packageManager field when found within a node_modules vend
});
});

it(`should use hash from "packageManager" even when "devEngines" defines a different one`, async () => {
Comment thread
aduh95 marked this conversation as resolved.
await xfs.mktempPromise(async cwd => {
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), {
packageManager: `yarn@3.0.0-rc.2+sha1.11111`,
devEngines: {
packageManager: {
name: `yarn`,
version: `3.0.0-rc.2+sha1.22222`,
},
},
});

await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
exitCode: 1,
stderr: expect.stringContaining(`Mismatch hashes. Expected 11111, got`),
stdout: ``,
});
});
});

describe(`should accept range in devEngines only if a specific version is provided`, () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behavior breaks existing projects which only define a range in devEngines (e.g. to make developers all use same major line of the package manager).
Normally that would be somewhat ok as its only your project and you know what you are doing, but this affects a lot of different tools like dependabot that are now broken for projects that not use corepack themself.

it(`either in package.json#packageManager field`, async () => {
await xfs.mktempPromise(async cwd => {
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), {
devEngines: {
packageManager: {
name: `pnpm`,
version: `6.x`,
},
},
});
await expect(runCli(cwd, [`pnpm`, `--version`])).resolves.toMatchObject({
exitCode: 1,
stderr: `Invalid package manager specification in package.json (pnpm@6.x); expected a semver version\n`,
stdout: ``,
});

await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), {
devEngines: {
packageManager: {
name: `pnpm`,
version: `6.x`,
},
},
packageManager: `pnpm@6.6.2+sha224.eb5c0acad3b0f40ecdaa2db9aa5a73134ad256e17e22d1419a2ab073`,
});
await expect(runCli(cwd, [`pnpm`, `--version`])).resolves.toMatchObject({
exitCode: 0,
stderr: ``,
stdout: `6.6.2\n`,
});
});
});
});

describe(`should reject if range in devEngines does not match version provided`, () => {
it(`in package.json#packageManager field`, async () => {
await xfs.mktempPromise(async cwd => {
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as PortablePath), {
devEngines: {
packageManager: {
name: `pnpm`,
version: `10.x`,
},
},
packageManager: `pnpm@6.6.2+sha1.7b4d6b176c1b93b5670ed94c24babb7d80c13854`,
});
await expect(runCli(cwd, [`pnpm`, `--version`])).resolves.toMatchObject({
exitCode: 1,
stderr: `"packageManager" field is set to "pnpm@6.6.2+sha1.7b4d6b176c1b93b5670ed94c24babb7d80c13854" which does not match the value defined in "devEngines.packageManager" for "pnpm" of "10.x"\n`,
stdout: ``,
});
});
});
});

it(`should use the closest matching packageManager field`, async () => {
await xfs.mktempPromise(async cwd => {
await xfs.mkdirPromise(ppath.join(cwd, `foo` as PortablePath), {recursive: true});
Expand Down