Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,9 +298,13 @@ same major line. Should you need to upgrade to a new major, use an explicit

- `COREPACK_ENABLE_AUTO_PIN` can be set to `1` to instruct Corepack to
update the `packageManager` field when it detects that the local package
doesn't list it. In general we recommend to always list a `packageManager`
field (which you can easily set through `corepack use [name]@[version]`), as
it ensures that your project installs are always deterministic.
doesn't list it. Auto-pinning is skipped if a lockfile for another package
manager (`package-lock.json`, `npm-shrinkwrap.json`, `pnpm-lock.yaml`, or
`yarn.lock`) exists alongside that `package.json`. Use `corepack use` to
explicitly select a different package manager. In general we recommend to
always list a `packageManager` field (which you can easily set through
`corepack use [name]@[version]`), as it ensures that your project installs
are always deterministic.

- `COREPACK_ENABLE_DOWNLOAD_PROMPT` can be set to `0` to
prevent Corepack showing the URL when it needs to download software, or can be
Expand Down
23 changes: 22 additions & 1 deletion sources/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,27 @@ export async function activatePackageManager(lastKnownGood: Record<string, strin
await createLastKnownGoodFile(lastKnownGood);
}

function hasConflictingLockfile(cwd: string, packageManager: string) {
const lockfiles = {
npm: [`package-lock.json`, `npm-shrinkwrap.json`],
pnpm: [`pnpm-lock.yaml`],
yarn: [`yarn.lock`],
};

for (const [name, filenames] of Object.entries(lockfiles)) {
if (name === packageManager)
continue;

const lockfile = filenames.find(filename => fs.existsSync(path.join(cwd, filename)));
if (lockfile) {
debugUtils.log(`Skipping auto-pin of ${packageManager} because ${path.join(cwd, lockfile)} belongs to ${name}`);
return true;
}
}

return false;
}

export class Engine {
config: Config;

Expand Down Expand Up @@ -287,7 +308,7 @@ export class Engine {
else if (typeof locator.reference === `function`)
fallbackDescriptor.range = await locator.reference();

if (process.env.COREPACK_ENABLE_AUTO_PIN === `1`) {
if (process.env.COREPACK_ENABLE_AUTO_PIN === `1` && !hasConflictingLockfile(path.dirname(result.target), fallbackDescriptor.name)) {
const resolved = await this.resolveDescriptor(fallbackDescriptor, {allowTags: true});
if (resolved === null)
throw new UsageError(`Failed to successfully resolve '${fallbackDescriptor.range}' to a valid ${fallbackDescriptor.name} release`);
Expand Down
120 changes: 120 additions & 0 deletions tests/AutoPin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import type {Filename} from '@yarnpkg/fslib';
import {npath, ppath, xfs} from '@yarnpkg/fslib';
import {beforeEach, describe, expect, it} from 'vitest';

import {runCli} from './_runCli.ts';

const manifest = `{\n "private": true\n}\n`;

beforeEach(async () => {
const home = await xfs.mktempPromise();
process.env.COREPACK_HOME = npath.fromPortablePath(home);
process.env.COREPACK_DEFAULT_TO_LATEST = `0`;
process.env.COREPACK_ENABLE_AUTO_PIN = `1`;
process.env.AUTH_TYPE = `COREPACK_NPM_TOKEN`;
process.env.TEST_INTEGRITY = `valid`;

return async () => {
await xfs.removePromise(home, {recursive: true});
};
});

describe.each([`yarn`, `pnpm`])(`auto-pinning %s`, name => {
it.each([
{lockfile: `package-lock.json`, manager: `npm`},
{lockfile: `npm-shrinkwrap.json`, manager: `npm`},
{lockfile: `pnpm-lock.yaml`, manager: `pnpm`},
{lockfile: `yarn.lock`, manager: `yarn`},
])(`respects $lockfile`, async ({lockfile, manager}) => {
await xfs.mktempPromise(async cwd => {
const manifestPath = ppath.join(cwd, `package.json`);
const lockfilePath = ppath.join(cwd, lockfile as Filename);
await xfs.writeFilePromise(manifestPath, manifest);
await xfs.writeFilePromise(lockfilePath, `existing lockfile\n`);

await expect(runCli(cwd, [`${name}@1.9998.9999`, `--version`], true)).resolves.toMatchObject({
exitCode: 0,
stdout: `${name}: Hello from custom registry\n`,
stderr: manager === name ? expect.stringContaining(`Corepack will now add one referencing ${name}@`) : ``,
});

if (manager === name) {
await expect(xfs.readJsonPromise(manifestPath)).resolves.toMatchObject({
packageManager: expect.stringMatching(new RegExp(`^${name}@1\\.9998\\.9999\\+sha512\\.`)),
});
} else {
await expect(xfs.readFilePromise(manifestPath, `utf8`)).resolves.toBe(manifest);
}
await expect(xfs.readFilePromise(lockfilePath, `utf8`)).resolves.toBe(`existing lockfile\n`);
});
});
});

it(`checks lockfiles alongside the selected manifest when invoked from a subdirectory`, async () => {
await xfs.mktempPromise(async cwd => {
const manifestPath = ppath.join(cwd, `package.json`);
const subdirectory = ppath.join(cwd, `src`);
await xfs.mkdirPromise(subdirectory);
await xfs.writeFilePromise(manifestPath, manifest);
await xfs.writeFilePromise(ppath.join(cwd, `package-lock.json`), `{}`);

await expect(runCli(subdirectory, [`pnpm@1.9998.9999`, `--version`], true)).resolves.toMatchObject({
exitCode: 0,
stderr: ``,
stdout: `pnpm: Hello from custom registry\n`,
});
await expect(xfs.readFilePromise(manifestPath, `utf8`)).resolves.toBe(manifest);
});
});

it(`skips auto-pinning if both matching and conflicting lockfiles exist`, async () => {
await xfs.mktempPromise(async cwd => {
const manifestPath = ppath.join(cwd, `package.json`);
await xfs.writeFilePromise(manifestPath, manifest);
await xfs.writeFilePromise(ppath.join(cwd, `pnpm-lock.yaml`), ``);
await xfs.writeFilePromise(ppath.join(cwd, `yarn.lock`), ``);

await expect(runCli(cwd, [`pnpm@1.9998.9999`, `--version`], true)).resolves.toMatchObject({
exitCode: 0,
stderr: ``,
});
await expect(xfs.readFilePromise(manifestPath, `utf8`)).resolves.toBe(manifest);
});
});

it(`allows explicitly selecting another package manager with corepack use`, async () => {
await xfs.mktempPromise(async cwd => {
const manifestPath = ppath.join(cwd, `package.json`);
await xfs.writeFilePromise(manifestPath, manifest);
await xfs.writeFilePromise(ppath.join(cwd, `package-lock.json`), `{}`);

await expect(runCli(cwd, [`use`, `pnpm@1.9998.9999`], true)).resolves.toMatchObject({
exitCode: 0,
stderr: ``,
});
await expect(xfs.readJsonPromise(manifestPath)).resolves.toMatchObject({
packageManager: expect.stringMatching(/^pnpm@1\.9998\.9999\+sha512\./),
});
});
});

it.each([`package-lock.json`, `yarn.lock`])(`respects %s when auto-pinning npm`, async lockfile => {
await xfs.mktempPromise(async cwd => {
const manifestPath = ppath.join(cwd, `package.json`);
await xfs.writeFilePromise(manifestPath, manifest);
await xfs.writeFilePromise(ppath.join(cwd, lockfile as Filename), ``);

await expect(runCli(cwd, [`npm@6.14.2`, `--version`])).resolves.toMatchObject({
exitCode: 0,
stdout: `6.14.2\n`,
stderr: lockfile === `package-lock.json` ? expect.stringContaining(`Corepack will now add one referencing npm@`) : ``,
});
if (lockfile === `package-lock.json`) {
await expect(xfs.readJsonPromise(manifestPath)).resolves.toMatchObject({
packageManager: expect.stringMatching(/^npm@6\.14\.2\+sha512\./),
});
} else {
await expect(xfs.readFilePromise(manifestPath, `utf8`)).resolves.toBe(manifest);
}
});
});