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: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,12 +271,20 @@ calling their respective binaries outside of projects defining the
Download the selected package managers and store them inside a tarball
suitable for use with `corepack install -g`.

### `corepack use <name[@<version>]>`
### `corepack use <name[@<version>]> [--here]`

When run, this command will retrieve the latest release matching the provided
descriptor, assign it to the project's package.json file, and automatically
perform an install.

Use `--here` for an independent project nested inside another project. For
example, running `corepack use pnpm@latest --here` from `examples/pnpm` updates
or creates `examples/pnpm/package.json`, even if a parent directory already
defines a package manager. Corepack validates the current manifest's
`devEngines.packageManager` and reads the current directory's `.corepack.env`,
without searching parent directories. Installation runs in the current directory.
Without `--here`, Corepack uses its normal project lookup.

### `corepack up`

Retrieve the latest available version for the current major release line of
Expand Down
10 changes: 5 additions & 5 deletions sources/commands/Base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import type {Context} from '../main.ts';
import * as specUtils from '../specUtils.ts';

export abstract class BaseCommand extends Command<Context> {
async resolvePatternsToDescriptors({patterns}: {patterns: Array<string>}) {
async resolvePatternsToDescriptors({patterns, localOnly = false}: {patterns: Array<string>, localOnly?: boolean}) {
const resolvedSpecs = patterns.map(pattern => specUtils.parseSpec(pattern, `CLI arguments`, {enforceExactVersion: false}));

if (resolvedSpecs.length === 0) {
const lookup = await specUtils.loadSpecAndEnv(this.context.cwd);
const lookup = await specUtils.loadSpecAndEnv(this.context.cwd, {localOnly});
switch (lookup.type) {
case `NoProject`:
throw new UsageError(`Couldn't find a project in the local directory - please specify the package manager to pack, or run this command from a valid project`);
Expand All @@ -29,16 +29,16 @@ export abstract class BaseCommand extends Command<Context> {
}
}
} else {
await specUtils.loadSpecAndEnv(this.context.cwd, {envOnly: true});
await specUtils.loadSpecAndEnv(this.context.cwd, {envOnly: true, localOnly});
}

return resolvedSpecs;
}

async setAndInstallLocalPackageManager(info: PreparedPackageManagerInfo) {
async setAndInstallLocalPackageManager(info: PreparedPackageManagerInfo, {localOnly = false} = {}) {
const {
previousPackageManager,
} = await specUtils.setLocalPackageManager(this.context.cwd, info);
} = await specUtils.setLocalPackageManager(this.context.cwd, info, {localOnly});

const command = this.context.engine.getPackageManagerSpecFor(info.locator).commands?.use ?? null;
if (command === null)
Expand Down
11 changes: 10 additions & 1 deletion sources/commands/Use.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ export class UseCommand extends BaseCommand {
When run, this command will retrieve the latest release matching the
provided descriptor, assign it to the project's package.json file, and
automatically perform an install.

With \`--here\`, treat the current directory as the project root. Update
or create its package.json and read its local .corepack.env without
searching parent directories.
`,
examples: [[
`Configure the project to use the latest Yarn release`,
Expand All @@ -22,9 +26,14 @@ export class UseCommand extends BaseCommand {

pattern = Option.String();

here = Option.Boolean(`--here`, false, {
description: `Update or create package.json in the current directory`,
});

async execute() {
const [descriptor] = await this.resolvePatternsToDescriptors({
patterns: [this.pattern],
localOnly: this.here,
});

const resolved = await this.context.engine.resolveDescriptor(descriptor, {allowTags: true, useCache: false});
Expand All @@ -34,6 +43,6 @@ export class UseCommand extends BaseCommand {
this.context.stdout.write(`Installing ${resolved.name}@${resolved.reference} in the project...\n`);

const packageManagerInfo = await this.context.engine.ensurePackageManager(resolved);
await this.setAndInstallLocalPackageManager(packageManagerInfo);
await this.setAndInstallLocalPackageManager(packageManagerInfo, {localOnly: this.here});
}
}
10 changes: 5 additions & 5 deletions sources/specUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ function parsePackageJSON(packageJSONContent: CorepackPackageJSON): ParsedPackag
return {packageManagerField: pm};
}

export async function setLocalPackageManager(cwd: string, info: PreparedPackageManagerInfo) {
const lookup = await loadSpecAndEnv(cwd);
export async function setLocalPackageManager(cwd: string, info: PreparedPackageManagerInfo, {localOnly = false} = {}) {
const lookup = await loadSpecAndEnv(cwd, {localOnly});

const projectFound = lookup.type !== `NoProject`;
const devEnginesValue = projectFound ? lookup.devEnginesValue : undefined;
Expand Down Expand Up @@ -193,7 +193,7 @@ async function loadEnvFileIfExists(cwd: string): Promise<{env: LocalEnvFile, pat
return void 0;
}

export async function loadSpecAndEnv(initialCwd: string, {envOnly} = {envOnly: false}): Promise<LoadSpecResult> {
export async function loadSpecAndEnv(initialCwd: string, {envOnly = false, localOnly = false} = {}): Promise<LoadSpecResult> {
let nextCwd = initialCwd;
let currCwd = ``;

Expand All @@ -205,9 +205,9 @@ export async function loadSpecAndEnv(initialCwd: string, {envOnly} = {envOnly: f

while (nextCwd !== currCwd && (!selection || !selection.data.packageManager)) {
currCwd = nextCwd;
nextCwd = path.dirname(currCwd);
nextCwd = localOnly ? currCwd : path.dirname(currCwd);

if (nodeModulesRegExp.test(currCwd))
if (!localOnly && nodeModulesRegExp.test(currCwd))
continue;

if (process.env.COREPACK_ENV_FILE !== `0` && !localEnv)
Expand Down
2 changes: 1 addition & 1 deletion tests/Use.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe(`UseCommand`, () => {
await expect(runCli(cwd, [`use`, `yarn@1.22.4`])).resolves.toMatchObject({
exitCode: 1,
stderr: ``,
stdout: `Installing yarn@1.22.4 in the project...\nUsage Error: The requested version of yarn@1.22.4+sha512.a1833b862fe52169bd6c2a033045a07df5bc6a23595c259e675fed1b2d035ab37abe6ce309720abb6636d68f03615054b6292dc0a70da31c8697fda228b50d18 does not match the devEngines specification (yarn@2.x)\n\n$ corepack use <pattern>\n`,
stdout: `Installing yarn@1.22.4 in the project...\nUsage Error: The requested version of yarn@1.22.4+sha512.a1833b862fe52169bd6c2a033045a07df5bc6a23595c259e675fed1b2d035ab37abe6ce309720abb6636d68f03615054b6292dc0a70da31c8697fda228b50d18 does not match the devEngines specification (yarn@2.x)\n\n$ corepack use [--here] <pattern>\n`,
});

// Should accept setting to a compatible version:
Expand Down
113 changes: 113 additions & 0 deletions tests/UseHere.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import {npath, ppath, xfs} from '@yarnpkg/fslib';
import {beforeEach, describe, expect, it} from 'vitest';

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

beforeEach(async () => {
const home = await xfs.mktempPromise();
process.env.COREPACK_HOME = npath.fromPortablePath(home);
process.env.COREPACK_DEFAULT_TO_LATEST = `0`;
process.env.AUTH_TYPE = `COREPACK_NPM_TOKEN`;
process.env.TEST_INTEGRITY = `valid`;
return () => xfs.removePromise(home, {recursive: true});
});

describe(`use --here`, () => {
it.each([true, false])(`targets the current directory (manifest exists: %s)`, async exists => {
await xfs.mktempPromise(async root => {
const parent = `{"packageManager":"yarn@1.22.4"}\n`;
await xfs.writeFilePromise(ppath.join(root, `package.json`), parent);
const cwd = ppath.join(root, `examples`, `pnpm`);
await xfs.mkdirpPromise(cwd);
if (exists)
await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), {name: `example`});

await expect(runCli(cwd, [`use`, `pnpm@1.9998.9999`, `--here`], true)).resolves.toMatchObject({
exitCode: 0,
stdout: expect.stringContaining(`pnpm: Hello from custom registry`),
stderr: ``,
});
await expect(xfs.readFilePromise(ppath.join(root, `package.json`), `utf8`)).resolves.toBe(parent);
await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toMatchObject({
...(exists ? {name: `example`} : {}),
packageManager: expect.stringMatching(/^pnpm@1\.9998\.9999\+sha512\.[0-9a-f]{128}$/),
});
});
});

it.each([
`{"devEngines":{"packageManager":{"name":"yarn","version":"2.x"}}}`,
`{invalid JSON`,
])(`ignores parent manifest %s`, async parent => {
await xfs.mktempPromise(async root => {
await xfs.writeFilePromise(ppath.join(root, `package.json`), parent);
const cwd = ppath.join(root, `child`);
await xfs.mkdirPromise(cwd);
await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), {});
await expect(runCli(cwd, [`use`, `--here`, `pnpm@1.9998.9999`], true)).resolves.toMatchObject({exitCode: 0});
await expect(xfs.readFilePromise(ppath.join(root, `package.json`), `utf8`)).resolves.toBe(parent);
});
});

it.each([{flags: []}, {flags: [`--no-here`]}])(`retains normal parent lookup with $flags`, async ({flags}) => {
await xfs.mktempPromise(async root => {
await xfs.writeJsonPromise(ppath.join(root, `package.json`), {packageManager: `yarn@1.22.4`});
const cwd = ppath.join(root, `child`);
await xfs.mkdirPromise(cwd);
const child = `{ "name": "child" }\n`;
await xfs.writeFilePromise(ppath.join(cwd, `package.json`), child);
await expect(runCli(cwd, [`use`, ...flags, `pnpm@1.9998.9999`], true)).resolves.toMatchObject({exitCode: 0});
await expect(xfs.readFilePromise(ppath.join(cwd, `package.json`), `utf8`)).resolves.toBe(child);
await expect(xfs.readJsonPromise(ppath.join(root, `package.json`))).resolves.toMatchObject({
packageManager: expect.stringMatching(/^pnpm@1\.9998\.9999\+/),
});
});
});

it(`validates the current manifest's devEngines constraints`, async () => {
await xfs.mktempPromise(async root => {
await xfs.writeJsonPromise(ppath.join(root, `package.json`), {packageManager: `pnpm@1.0.0`});
const cwd = ppath.join(root, `child`);
await xfs.mkdirPromise(cwd);
const child = `{"devEngines":{"packageManager":{"name":"yarn","version":"2.x"}}}`;
await xfs.writeFilePromise(ppath.join(cwd, `package.json`), child);
await expect(runCli(cwd, [`use`, `--here`, `pnpm@1.9998.9999`], true)).resolves.toMatchObject({
exitCode: 1,
stdout: expect.stringContaining(`does not match the devEngines specification (yarn@2.x)`),
});
await expect(xfs.readFilePromise(ppath.join(cwd, `package.json`), `utf8`)).resolves.toBe(child);
});
});

it.each([true, false])(`loads environment only from the current directory (local: %s)`, async local => {
await xfs.mktempPromise(async root => {
const cwd = ppath.join(root, `child`);
await xfs.mkdirPromise(cwd);
await xfs.writeFilePromise(ppath.join(local ? cwd : root, `.corepack.env`), `COREPACK_ENABLE_NETWORK=0\n`);
const result = await runCli(cwd, [`use`, `--here`, `pnpm@1.9998.9999`], true);
expect(result.exitCode).toBe(local ? 1 : 0);
if (local) {
expect(result.stdout).toContain(`Network access disabled`);
await expect(xfs.existsPromise(ppath.join(cwd, `package.json`))).resolves.toBe(false);
}
});
});

it(`runs the installation in the current directory`, async () => {
await xfs.mktempPromise(async root => {
const parent = `{"packageManager":"pnpm@1.0.0"}\n`;
await xfs.writeFilePromise(ppath.join(root, `package.json`), parent);
const cwd = ppath.join(root, `child`);
await xfs.mkdirPromise(cwd);
await xfs.writeJsonPromise(ppath.join(cwd, `package.json`), {
name: `child`,
license: `MIT`,
scripts: {postinstall: `node -e "require('fs').writeFileSync('installed-here', process.cwd())"`},
});
await expect(runCli(cwd, [`use`, `--here`, `yarn@1.22.4`])).resolves.toMatchObject({exitCode: 0});
await expect(xfs.readFilePromise(ppath.join(cwd, `installed-here`), `utf8`)).resolves.toBe(npath.fromPortablePath(cwd));
await expect(xfs.existsPromise(ppath.join(root, `installed-here`))).resolves.toBe(false);
await expect(xfs.readFilePromise(ppath.join(root, `package.json`), `utf8`)).resolves.toBe(parent);
});
});
});