Makes it possible to call arbitrary package manager versions from the Corepack CLI (#47)

This commit is contained in:
Maël Nison 2021-08-15 20:05:04 +02:00 committed by GitHub
parent 78d94eb297
commit d6e3e652ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 143 additions and 90 deletions

View File

@ -77,6 +77,12 @@ The utility commands detailed in the next section.
## Utility Commands
### `corepack <binary name>[@<version>] [... args]`
This meta-command runs the specified package manager in the local folder. You can use it to force an install to run with a given version, which can be useful when looking for regressions.
Note that those commands still check whether the local project is configured for the given package manager (ie you won't be able to run `corepack yarn install` on a project where the `packageManager` field references `pnpm`).
### `corepack enable [... name]`
| Option | Description |
@ -117,7 +123,7 @@ This command will retrieve the given package manager from the specified archive
## Contributing
If you want to build corepack yourself things yourself, you can build the project like this:
If you want to build corepack yourself, you can build the project like this:
1. Clone this repository
2. Run `yarn build` (no need for `yarn install`)

View File

@ -29,7 +29,7 @@ async function main() {
const entryPath = path.join(distDir, `${binaryName}.js`);
const entryScript = [
`#!/usr/bin/env node\n`,
`require('./corepack').runMain(['${packageManager}', '${binaryName}', ...process.argv.slice(2)]);\n`,
`require('./corepack').runMain(['${binaryName}', ...process.argv.slice(2)]);\n`,
].join(``);
fs.writeFileSync(entryPath, entryScript);

View File

@ -16,6 +16,22 @@ export class Engine {
constructor(public config: Config = defaultConfig as Config) {
}
getPackageManagerFor(binaryName: string): SupportedPackageManagers | null {
for (const packageManager of SupportedPackageManagerSet) {
for (const rangeDefinition of Object.values(this.config.definitions[packageManager]!.ranges)) {
const bins = Array.isArray(rangeDefinition.bin)
? rangeDefinition.bin
: Object.keys(rangeDefinition.bin);
if (bins.includes(binaryName)) {
return packageManager;
}
}
}
return null;
}
getBinariesFor(name: SupportedPackageManagers) {
const binNames = new Set<string>();

View File

@ -8,86 +8,98 @@ import {PrepareCommand} from './command
import * as miscUtils from './miscUtils';
import * as pmmUtils from './pmmUtils';
import * as specUtils from './specUtils';
import {Locator, isSupportedPackageManager} from './types';
import {Locator, SupportedPackageManagers, Descriptor} from './types';
export type CustomContext = {cwd: string, engine: Engine};
export type Context = BaseContext & CustomContext;
export async function main(argv: Array<string>, context: CustomContext & Partial<Context>) {
const firstArg = argv[0];
const [, candidatePackageManager, requestedVersion] = firstArg.match(/^([^@]*)(?:@(.*))?$/)!;
type PackageManagerRequest = {
packageManager: SupportedPackageManagers;
binaryName: string;
binaryVersion: string | null;
};
if (isSupportedPackageManager(candidatePackageManager)) {
const packageManager = candidatePackageManager;
const binaryName = argv[1];
function getPackageManagerRequestFromCli(parameter: string | undefined, context: CustomContext & Partial<Context>): PackageManagerRequest {
if (!parameter)
return null;
// Note: we're playing a bit with Clipanion here, since instead of letting it
// decide how to route the commands, we'll instead tweak the init settings
// based on the arguments.
const cli = new Cli<Context>({binaryName});
const defaultVersion = await context.engine.getDefaultVersion(packageManager);
const match = parameter.match(/^([^@]*)(?:@(.*))?$/);
if (!match)
return null;
class BinaryCommand extends Command<Context> {
proxy = Option.Proxy();
const [, binaryName, binaryVersion] = match;
const packageManager = context.engine.getPackageManagerFor(binaryName);
if (!packageManager)
return null;
async execute() {
const definition = context.engine.config.definitions[packageManager]!;
return {
packageManager,
binaryName,
binaryVersion: binaryVersion || null,
};
}
// If all leading segments match one of the patterns defined in the `transparent`
// key, we tolerate calling this binary even if the local project isn't explicitly
// configured for it, and we use the special default version if requested.
let isTransparentCommand = false;
for (const transparentPath of definition.transparent.commands) {
if (transparentPath[0] === binaryName && transparentPath.slice(1).every((segment, index) => segment === this.proxy[index])) {
isTransparentCommand = true;
break;
}
}
async function executePackageManagerRequest({packageManager, binaryName, binaryVersion}: PackageManagerRequest, args: Array<string>, context: Context) {
const defaultVersion = await context.engine.getDefaultVersion(packageManager);
const definition = context.engine.config.definitions[packageManager]!;
const fallbackReference = isTransparentCommand
? definition.transparent.default ?? defaultVersion
: defaultVersion;
const fallbackLocator: Locator = {
name: packageManager,
reference: fallbackReference,
};
let descriptor;
try {
descriptor = await specUtils.findProjectSpec(this.context.cwd, fallbackLocator, {transparent: isTransparentCommand});
} catch (err) {
if (err instanceof miscUtils.Cancellation) {
return 1;
} else {
throw err;
}
}
if (requestedVersion)
descriptor.range = requestedVersion;
const resolved = await context.engine.resolveDescriptor(descriptor, {allowTags: true});
if (resolved === null)
throw new UsageError(`Failed to successfully resolve '${descriptor.range}' to a valid ${descriptor.name} release`);
const installSpec = await context.engine.ensurePackageManager(resolved);
const exitCode = await pmmUtils.runVersion(installSpec, resolved, binaryName, this.proxy, this.context);
return exitCode;
}
// If all leading segments match one of the patterns defined in the `transparent`
// key, we tolerate calling this binary even if the local project isn't explicitly
// configured for it, and we use the special default version if requested.
let isTransparentCommand = false;
for (const transparentPath of definition.transparent.commands) {
if (transparentPath[0] === binaryName && transparentPath.slice(1).every((segment, index) => segment === args[index])) {
isTransparentCommand = true;
break;
}
}
cli.register(BinaryCommand);
const fallbackReference = isTransparentCommand
? definition.transparent.default ?? defaultVersion
: defaultVersion;
return await cli.run(argv.slice(2), {
...Cli.defaultContext,
...context,
});
} else {
const cli = new Cli<Context>({
const fallbackLocator: Locator = {
name: packageManager,
reference: fallbackReference,
};
let descriptor: Descriptor;
try {
descriptor = await specUtils.findProjectSpec(context.cwd, fallbackLocator, {transparent: isTransparentCommand});
} catch (err) {
if (err instanceof miscUtils.Cancellation) {
return 1;
} else {
throw err;
}
}
if (binaryVersion)
descriptor.range = binaryVersion;
const resolved = await context.engine.resolveDescriptor(descriptor, {allowTags: true});
if (resolved === null)
throw new UsageError(`Failed to successfully resolve '${descriptor.range}' to a valid ${descriptor.name} release`);
const installSpec = await context.engine.ensurePackageManager(resolved);
const exitCode = await pmmUtils.runVersion(installSpec, resolved, binaryName, args, context);
return exitCode;
}
export async function main(argv: Array<string>, context: CustomContext & Partial<Context>) {
const corepackVersion = require(`../package.json`).version;
const [firstArg, ...restArgs] = argv;
const request = getPackageManagerRequestFromCli(firstArg, context);
let cli: Cli<Context>;
if (!request) {
// If the first argument doesn't match any supported package manager, we fallback to the standard Corepack CLI
cli = new Cli({
binaryLabel: `Corepack`,
binaryName: `corepack`,
binaryVersion: require(`../package.json`).version,
binaryVersion: corepackVersion,
});
cli.register(Builtins.HelpCommand);
@ -102,6 +114,25 @@ export async function main(argv: Array<string>, context: CustomContext & Partial
...Cli.defaultContext,
...context,
});
} else {
// Otherwise, we create a single-command CLI to run the specified package manager (we still use Clipanion in order to pretty-print usage errors).
const cli = new Cli({
binaryLabel: `'${request.binaryName}', via Corepack`,
binaryName: request.binaryName,
binaryVersion: `corepack/${corepackVersion}`,
});
cli.register(class BinaryCommand extends Command<Context> {
proxy = Option.Proxy();
async execute() {
return executePackageManagerRequest(request, this.proxy, this.context);
}
});
return await cli.run(restArgs, {
...Cli.defaultContext,
...context,
});
}
}

View File

@ -24,7 +24,7 @@ for (const [name, version] of testedPackageManagers) {
packageManager: `${name}@${version}`,
});
await expect(runCli(cwd, [name, name, `--version`])).resolves.toMatchObject({
await expect(runCli(cwd, [name, `--version`])).resolves.toMatchObject({
exitCode: 0,
stdout: `${version}\n`,
});
@ -49,12 +49,12 @@ it(`should ignore the packageManager field when found within a node_modules vend
packageManager: `npm@6.14.2`,
});
await expect(runCli(ppath.join(cwd, `node_modules/foo` as PortablePath), [`yarn`, `yarn`, `--version`])).resolves.toMatchObject({
await expect(runCli(ppath.join(cwd, `node_modules/foo` as PortablePath), [`yarn`, `--version`])).resolves.toMatchObject({
exitCode: 0,
stdout: `1.22.4\n`,
});
await expect(runCli(ppath.join(cwd, `node_modules/@foo/bar` as PortablePath), [`yarn`, `yarn`, `--version`])).resolves.toMatchObject({
await expect(runCli(ppath.join(cwd, `node_modules/@foo/bar` as PortablePath), [`yarn`, `--version`])).resolves.toMatchObject({
exitCode: 0,
stdout: `1.22.4\n`,
});
@ -73,7 +73,7 @@ it(`should use the closest matching packageManager field`, async () => {
packageManager: `npm@6.14.2`,
});
await expect(runCli(ppath.join(cwd, `foo` as PortablePath), [`npm`, `npm`, `--version`])).resolves.toMatchObject({
await expect(runCli(ppath.join(cwd, `foo` as PortablePath), [`npm`, `--version`])).resolves.toMatchObject({
exitCode: 0,
stdout: `6.14.2\n`,
});
@ -86,7 +86,7 @@ it(`should expose its root to spawned processes`, async () => {
packageManager: `npm@6.14.2`,
});
await expect(runCli(cwd, [`npm`, `npm`, `run`, `env`])).resolves.toMatchObject({
await expect(runCli(cwd, [`npm`, `run`, `env`])).resolves.toMatchObject({
exitCode: 0,
stdout: expect.stringContaining(`COREPACK_ROOT=${npath.dirname(__dirname)}\n`),
});
@ -99,7 +99,7 @@ it(`shouldn't allow using regular Yarn commands on npm-configured projects`, asy
packageManager: `npm@6.14.2`,
});
await expect(runCli(cwd, [`yarn`, `yarn`, `--version`])).resolves.toMatchObject({
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
exitCode: 1,
});
});
@ -111,7 +111,7 @@ it(`should allow using transparent commands on npm-configured projects`, async (
packageManager: `npm@6.14.2`,
});
await expect(runCli(cwd, [`yarn`, `yarn`, `dlx`, `cat@0.2.0`, __filename])).resolves.toMatchObject({
await expect(runCli(cwd, [`yarn`, `dlx`, `cat@0.2.0`, __filename])).resolves.toMatchObject({
exitCode: 0,
});
});
@ -119,7 +119,7 @@ it(`should allow using transparent commands on npm-configured projects`, async (
it(`should transparently use the preconfigured version when there is no local project`, async () => {
await xfs.mktempPromise(async cwd => {
await expect(runCli(cwd, [`yarn`, `yarn`, `--version`])).resolves.toMatchObject({
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
exitCode: 0,
});
});
@ -135,17 +135,17 @@ it(`should use the pinned version when local projects don't list any spec`, asyn
// empty package.json file
});
await expect(runCli(cwd, [`yarn`, `yarn`, `--version`])).resolves.toMatchObject({
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
stdout: `${config.definitions.yarn.default}\n`,
exitCode: 0,
});
await expect(runCli(cwd, [`pnpm`, `pnpm`, `--version`])).resolves.toMatchObject({
await expect(runCli(cwd, [`pnpm`, `--version`])).resolves.toMatchObject({
stdout: `${config.definitions.pnpm.default}\n`,
exitCode: 0,
});
await expect(runCli(cwd, [`npm`, `npm`, `--version`])).resolves.toMatchObject({
await expect(runCli(cwd, [`npm`, `--version`])).resolves.toMatchObject({
stdout: `${config.definitions.npm.default}\n`,
exitCode: 0,
});
@ -162,7 +162,7 @@ it(`should allow updating the pinned version using the "prepare" command`, async
// empty package.json file
});
await expect(runCli(cwd, [`yarn`, `yarn`, `--version`])).resolves.toMatchObject({
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
stdout: `1.0.0\n`,
exitCode: 0,
});
@ -179,7 +179,7 @@ it(`should allow to call "prepare" without arguments within a configured project
exitCode: 0,
});
await expect(runCli(cwd, [`yarn`, `yarn`, `--version`])).resolves.toMatchObject({
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
stdout: `1.0.0\n`,
exitCode: 0,
});
@ -199,17 +199,17 @@ it(`should allow to call "prepare" with --all to prepare all package managers`,
process.env.COREPACK_ENABLE_NETWORK = `0`;
try {
await expect(runCli(cwd, [`yarn`, `yarn`, `--version`])).resolves.toMatchObject({
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
stdout: `${config.definitions.yarn.default}\n`,
exitCode: 0,
});
await expect(runCli(cwd, [`pnpm`, `pnpm`, `--version`])).resolves.toMatchObject({
await expect(runCli(cwd, [`pnpm`, `--version`])).resolves.toMatchObject({
stdout: `${config.definitions.pnpm.default}\n`,
exitCode: 0,
});
await expect(runCli(cwd, [`npm`, `npm`, `--version`])).resolves.toMatchObject({
await expect(runCli(cwd, [`npm`, `--version`])).resolves.toMatchObject({
stdout: `${config.definitions.npm.default}\n`,
exitCode: 0,
});
@ -228,7 +228,7 @@ it(`should support disabling the network accesses from the environment`, async (
packageManager: `yarn@2.2.2`,
});
await expect(runCli(cwd, [`yarn`, `yarn`, `--version`])).resolves.toMatchObject({
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
stdout: expect.stringContaining(`Network access disabled by the environment`),
exitCode: 1,
});
@ -260,7 +260,7 @@ it(`should support hydrating package managers from cached archives`, async () =>
packageManager: `yarn@2.2.2`,
});
await expect(runCli(cwd, [`yarn`, `yarn`, `--version`])).resolves.toMatchObject({
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
stdout: `2.2.2\n`,
exitCode: 0,
});
@ -292,7 +292,7 @@ it(`should support hydrating multiple package managers from cached archives`, as
packageManager: `yarn@2.2.2`,
});
await expect(runCli(cwd, [`yarn`, `yarn`, `--version`])).resolves.toMatchObject({
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
stdout: `2.2.2\n`,
exitCode: 0,
});
@ -301,7 +301,7 @@ it(`should support hydrating multiple package managers from cached archives`, as
packageManager: `pnpm@5.8.0`,
});
await expect(runCli(cwd, [`pnpm`, `pnpm`, `--version`])).resolves.toMatchObject({
await expect(runCli(cwd, [`pnpm`, `--version`])).resolves.toMatchObject({
stdout: `5.8.0\n`,
exitCode: 0,
});
@ -317,12 +317,12 @@ it(`should support running package managers with bin array`, async () => {
packageManager: `yarn@2.2.2`,
});
await expect(runCli(cwd, [`yarn`, `yarnpkg`, `--version`])).resolves.toMatchObject({
await expect(runCli(cwd, [`yarnpkg`, `--version`])).resolves.toMatchObject({
stdout: `2.2.2\n`,
exitCode: 0,
});
await expect(runCli(cwd, [`yarn`, `yarn`, `--version`])).resolves.toMatchObject({
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
stdout: `2.2.2\n`,
exitCode: 0,
});