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

feat(use): add --from-npm CLI flag #358

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,11 +211,13 @@ 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>]> [--from-npm]`

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.
When passing the `--from-npm` flag, Corepack will use the latest version of the
package with the corresponding name from the npm registry.

### `corepack up`

Expand Down
13 changes: 10 additions & 3 deletions sources/commands/Base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,21 @@ export abstract class BaseCommand extends Command<Context> {
const {data, indent} = nodeUtils.readPackageJson(content);

const previousPackageManager = data.packageManager ?? `unknown`;
data.packageManager = `${info.locator.name}@${info.locator.reference}+${info.hash}`;
data.packageManager = `${info.locator.name}@${info.locator.reference}${URL.canParse(info.locator.reference) ? `#` : `+`}${info.hash}`;

const newContent = nodeUtils.normalizeLineEndings(content, `${JSON.stringify(data, null, indent)}\n`);
await fs.promises.writeFile(lookup.target, newContent, `utf8`);

const command = this.context.engine.getPackageManagerSpecFor(info.locator).commands?.use ?? null;
if (command === null)
let command: Array<string>;
try {
const _command = this.context.engine.getPackageManagerSpecFor(info.locator).commands?.use;
if (_command == null)
return 0;

command = _command;
} catch {
return 0;
}

// Adding it into the environment avoids breaking package managers that
// don't expect those options.
Expand Down
52 changes: 44 additions & 8 deletions sources/commands/Use.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import {Command, Option, UsageError} from 'clipanion';


import {fetchLatestStableVersion} from '../corepackUtils';

import {BaseCommand} from './Base';


export class UseCommand extends BaseCommand {
static paths = [
[`use`],
];

fromNpm = Option.Boolean(`--from-npm`, false, {
description: `If true, the package manager will be installed from the npm registry`,
});

static usage = Command.Usage({
description: `Define the package manager to use for the current project`,
details: `
Expand All @@ -17,23 +25,51 @@ export class UseCommand extends BaseCommand {
examples: [[
`Configure the project to use the latest Yarn release`,
`corepack use yarn`,
], [
`Configure the project to use the latest Yarn release available from the "yarn" package on the npm registry`,
`corepack use yarn --from-npm`,
]],
});

pattern = Option.String();

async execute() {
const [descriptor] = await this.resolvePatternsToDescriptors({
patterns: [this.pattern],
});
let packageManagerInfo: Parameters<typeof this.setLocalPackageManager>[0];
if (this.fromNpm) {
const registry = {
type: `npm` as const,
package: this.pattern,
};
const versionWithHash = await fetchLatestStableVersion(registry);
const [version, hash] = versionWithHash.split(`+`);
const location = `https://registry.npmjs.com/${this.pattern}/-/${this.pattern}-${version}.tgz`;
packageManagerInfo = {
locator: {
name: this.pattern,
reference: location,
},
spec: {
bin: {},
registry,
url: location,
},
hash,
location,
bin: undefined,
};
} else {
const [descriptor] = await this.resolvePatternsToDescriptors({
patterns: [this.pattern],
});

const resolved = await this.context.engine.resolveDescriptor(descriptor, {allowTags: true, useCache: false});
if (resolved === null)
throw new UsageError(`Failed to successfully resolve '${descriptor.range}' to a valid ${descriptor.name} release`);
const resolved = await this.context.engine.resolveDescriptor(descriptor, {allowTags: true, useCache: false});
if (resolved === null)
throw new UsageError(`Failed to successfully resolve '${descriptor.range}' to a valid ${descriptor.name} release`);

this.context.stdout.write(`Installing ${resolved.name}@${resolved.reference} in the project...\n`);
this.context.stdout.write(`Installing ${resolved.name}@${resolved.reference} in the project...\n`);

const packageManagerInfo = await this.context.engine.ensurePackageManager(resolved);
packageManagerInfo = await this.context.engine.ensurePackageManager(resolved);
}
await this.setLocalPackageManager(packageManagerInfo);
}
}
4 changes: 2 additions & 2 deletions sources/specUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function parseSpec(raw: unknown, source: string, {enforceExactVersion = t

const name = atIndex === -1 ? raw : raw.slice(0, -1);
if (!isSupportedPackageManager(name))
throw new UsageError(`Unsupported package manager specification (${name})`);
throw new UsageError(`Unsupported package manager specification (${name}). Consider using the \`--from-npm\` flag if you meant to use the npm package \`${name}\` as your package manager`);

return {
name, range: `*`,
Expand All @@ -33,7 +33,7 @@ export function parseSpec(raw: unknown, source: string, {enforceExactVersion = t
const isURL = URL.canParse(range);
if (!isURL) {
if (enforceExactVersion && !semver.valid(range))
throw new UsageError(`Invalid package manager specification in ${source} (${raw}); expected a semver version${enforceExactVersion ? `` : `, range, or tag`}`);
throw new UsageError(`Invalid package manager specification in ${source} (${raw}); expected a semver version`);

if (!isSupportedPackageManager(name)) {
throw new UsageError(`Unsupported package manager specification (${raw})`);
Expand Down
19 changes: 19 additions & 0 deletions tests/Use.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,23 @@ describe(`UseCommand`, () => {
});
});
});

it(`should accept --from-npm CLI flag`, async () => {
await xfs.mktempPromise(async cwd => {
await expect(runCli(cwd, [`use`, `test`])).resolves.toMatchObject({
exitCode: 1,
stdout: /Invalid package manager specification in CLI arguments. Consider adding the `--from-npm` flag if you meant to use the npm package `test` as your package manager/,
stderr: ``,
});

await expect(runCli(cwd, [`use`, `test`, `--from-npm`])).resolves.toMatchObject({
exitCode: 0,
stderr: ``,
});

await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toMatchObject({
packageManager: `test@https://registry.npmjs.com/test/-/test-3.3.0.tgz#sha1.a2b56c6aa386c5732065793e8d9d92074a9cdd41`,
});
});
});
});
1 change: 1 addition & 0 deletions tests/nock/fsYZFJKYLkFfDv-Jyd3j2w-2.dat

Large diffs are not rendered by default.