mirror of https://github.com/nodejs/corepack.git
Makes it possible to call arbitrary package manager versions from the Corepack CLI (#47)
This commit is contained in:
parent
78d94eb297
commit
d6e3e652ed
|
|
@ -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`)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
||||
|
|
|
|||
163
sources/main.ts
163
sources/main.ts
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue