Skip to content

Commit

Permalink
Refactoring of the CLI interface (#291)
Browse files Browse the repository at this point in the history
* Refactoring of the CLI interface

* Updates the Nock snapshots

* Regenerates the Nock files on Node 16

* Update README.md

* Adds --cache-only to corepack install -g

* Fixes hash generation
  • Loading branch information
arcanis authored Aug 28, 2023
1 parent b8a4a52 commit fe3e5cd
Show file tree
Hide file tree
Showing 36 changed files with 751 additions and 180 deletions.
70 changes: 46 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,26 +92,25 @@ If there is no Known Good Release for the requested package manager, Corepack
looks up the npm registry for the latest available version and cache it for
future use.

The Known Good Releases can be updated system-wide using the `--activate` flag
from the `corepack prepare` and `corepack hydrate` commands.
The Known Good Releases can be updated system-wide using `corepack install -g`.

## Offline Workflow

The utility commands detailed in the next section.

- Either you can use the network while building your container image, in which
case you'll simply run `corepack prepare` to make sure that your image
case you'll simply run `corepack pack` to make sure that your image
includes the Last Known Good release for the specified package manager.

- If you want to have _all_ Last Known Good releases for all package managers,
just use the `--all` flag which will do just that.

- Or you're publishing your project to a system where the network is
unavailable, in which case you'll preemptively generate a package manager
archive from your local computer (using `corepack prepare -o`) before storing
archive from your local computer (using `corepack pack -o`) before storing
it somewhere your container will be able to access (for example within your
repository). After that it'll just be a matter of running
`corepack hydrate <path/to/corepack.tgz>` to setup the cache.
`corepack install -g --cache-only <path/to/corepack.tgz>` to setup the cache.

## Utility Commands

Expand Down Expand Up @@ -171,29 +170,52 @@ echo "function npx { corepack npx `$args }" >> $PROFILE
This command will detect where Node.js is installed and will remove the shims
from there.

### `corepack prepare [... name@version]`
### `corepack install`

| Option | Description |
| ------------- | ----------------------------------------------------------------------- |
| `--all` | Prepare the "Last Known Good" version of all supported package managers |
| `-o,--output` | Also generate an archive containing the package managers |
| `--activate` | Also update the "Last Known Good" release |
Download and install the package manager configured in the local project.
This command doesn't change the global version used when running the package
manager from outside the project (use the \`-g,--global\` flag if you wish
to do this).

This command will download the given package managers (or the one configured for
the local project if no argument is passed in parameter) and store it within the
Corepack cache. If the `-o,--output` flag is set (optionally with a path as
parameter), an archive will also be generated that can be used by the
`corepack hydrate` command.
### `corepack install <-g,--global> [--all] [... name@version]`

### `corepack hydrate <path/to/corepack.tgz>`
| Option | Description |
| --------------------- | ------------------------------------------ |
| `--all` | Install all Last Known Good releases |

Install the selected package managers and install them on the system.

Package managers thus installed will be configured as the new default when
calling their respective binaries outside of projects defining the
`packageManager` field.

### `corepack pack [--all] [... name@version]`

| Option | Description |
| --------------------- | ------------------------------------------ |
| `--all` | Pack all Last Known Good releases |
| `--json ` | Print the output folder rather than logs |
| `-o,--output ` | Path where to generate the archive |

Download the selected package managers and store them inside a tarball
suitable for use with `corepack install -g`.

### `corepack use <name@version>`

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.

### `corepack up`

| Option | Description |
| ------------ | ----------------------------------------- |
| `--activate` | Also update the "Last Known Good" release |
Retrieve the latest available version for the current major release line of
the package manager used in the local project, and update the project to use
it.

This command will retrieve the given package manager from the specified archive
and will install it within the Corepack cache, ready to be used without further
network interaction.
Unlike `corepack use` this command doesn't take a package manager name nor a
version range, as it will always select the latest available version from the
same major line. Should you need to upgrade to a new major, use an explicit
`corepack use {name}@latest` call.

## Environment Variables

Expand All @@ -204,7 +226,7 @@ network interaction.
- `COREPACK_ENABLE_NETWORK` can be set to `0` to prevent Corepack from accessing
the network (in which case you'll be responsible for hydrating the package
manager versions that will be required for the projects you'll run, using
`corepack hydrate`).
`corepack install -g --cache-only`).

- `COREPACK_ENABLE_STRICT` can be set to `0` to prevent Corepack from throwing
error if the package manager does not correspond to the one defined for the
Expand Down
15 changes: 15 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
"registry": {
"type": "npm",
"package": "npm"
},
"commands": {
"use": ["npm", "install"]
}
}
}
Expand Down Expand Up @@ -62,6 +65,9 @@
"registry": {
"type": "npm",
"package": "pnpm"
},
"commands": {
"use": ["pnpm", "install"]
}
},
">=6.0.0": {
Expand All @@ -73,6 +79,9 @@
"registry": {
"type": "npm",
"package": "pnpm"
},
"commands": {
"use": ["pnpm", "install"]
}
}
}
Expand Down Expand Up @@ -102,6 +111,9 @@
"registry": {
"type": "npm",
"package": "yarn"
},
"commands": {
"use": ["yarn", "install"]
}
},
">=2.0.0": {
Expand All @@ -118,6 +130,9 @@
"tags": "latest",
"versions": "tags"
}
},
"commands": {
"use": ["yarn", "install"]
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@
"@jest/globals": "^29.0.0",
"@types/debug": "^4.1.5",
"@types/jest": "^29.0.0",
"@types/node": "^20.0.0",
"@types/node": "^20.4.6",
"@types/semver": "^7.1.0",
"@types/tar": "^6.0.0",
"@types/which": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"@yarnpkg/eslint-config": "^0.6.0-rc.7",
"@yarnpkg/fslib": "^2.1.0",
"@yarnpkg/fslib": "^3.0.0-rc.48",
"@zkochan/cmd-shim": "^6.0.0",
"babel-plugin-dynamic-import-node": "^2.3.3",
"clipanion": "^3.0.1",
Expand Down
62 changes: 31 additions & 31 deletions sources/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import * as semverUtils from './semverUtil
import {Config, Descriptor, Locator} from './types';
import {SupportedPackageManagers, SupportedPackageManagerSet} from './types';

export type PreparedPackageManagerInfo = Awaited<ReturnType<Engine[`ensurePackageManager`]>>;

export class Engine {
constructor(public config: Config = defaultConfig as Config) {
Expand All @@ -33,6 +34,19 @@ export class Engine {
return null;
}

getPackageManagerSpecFor(locator: Locator) {
const definition = this.config.definitions[locator.name];
if (typeof definition === `undefined`)
throw new UsageError(`This package manager (${locator.name}) isn't supported by this corepack build`);

const ranges = Object.keys(definition.ranges).reverse();
const range = ranges.find(range => semverUtils.satisfiesWithPrereleases(locator.reference, range));
if (typeof range === `undefined`)
throw new Error(`Assertion failed: Specified resolution (${locator.reference}) isn't supported by any of ${ranges.join(`, `)}`);

return definition.ranges[range];
}

getBinariesFor(name: SupportedPackageManagers) {
const binNames = new Set<string>();

Expand Down Expand Up @@ -111,25 +125,23 @@ export class Engine {
}

async ensurePackageManager(locator: Locator) {
const definition = this.config.definitions[locator.name];
if (typeof definition === `undefined`)
throw new UsageError(`This package manager (${locator.name}) isn't supported by this corepack build`);

const ranges = Object.keys(definition.ranges).reverse();
const range = ranges.find(range => semverUtils.satisfiesWithPrereleases(locator.reference, range));
if (typeof range === `undefined`)
throw new Error(`Assertion failed: Specified resolution (${locator.reference}) isn't supported by any of ${ranges.join(`, `)}`);
const spec = this.getPackageManagerSpecFor(locator);

const installedLocation = await corepackUtils.installVersion(folderUtils.getInstallFolder(), locator, {
spec: definition.ranges[range],
const packageManagerInfo = await corepackUtils.installVersion(folderUtils.getInstallFolder(), locator, {
spec,
});

return {
location: installedLocation,
spec: definition.ranges[range],
...packageManagerInfo,
locator,
spec,
};
}

async fetchAvailableVersions() {

}

async resolveDescriptor(descriptor: Descriptor, {allowTags = false, useCache = true}: {allowTags?: boolean, useCache?: boolean} = {}) {
const definition = this.config.definitions[descriptor.name];
if (typeof definition === `undefined`)
Expand All @@ -138,7 +150,7 @@ export class Engine {
let finalDescriptor = descriptor;
if (!semver.valid(descriptor.range) && !semver.validRange(descriptor.range)) {
if (!allowTags)
throw new UsageError(`Packages managers can't be referended via tags in this context`);
throw new UsageError(`Packages managers can't be referenced via tags in this context`);

// We only resolve tags from the latest registry entry
const ranges = Object.keys(definition.ranges);
Expand All @@ -165,28 +177,16 @@ export class Engine {
if (semver.valid(finalDescriptor.range))
return {name: finalDescriptor.name, reference: finalDescriptor.range};

const candidateRangeDefinitions = Object.keys(definition.ranges).filter(range => {
return semverUtils.satisfiesWithPrereleases(finalDescriptor.range, range);
});

const tagResolutions = await Promise.all(candidateRangeDefinitions.map(async range => {
return [range, await corepackUtils.fetchAvailableVersions(definition.ranges[range].registry)] as const;
const versions = await Promise.all(Object.keys(definition.ranges).map(async range => {
const versions = await corepackUtils.fetchAvailableVersions(definition.ranges[range].registry);
return versions.filter(version => semverUtils.satisfiesWithPrereleases(version, finalDescriptor.range));
}));

// If a version is available under multiple strategies (for example if
// Yarn is published to both the v1 package and git), we only care
// about the latest one
const resolutionMap = new Map();
for (const [range, resolutions] of tagResolutions)
for (const entry of resolutions)
resolutionMap.set(entry, range);

const candidates = [...resolutionMap.keys()];
const maxSatisfying = semver.maxSatisfying(candidates, finalDescriptor.range);
if (maxSatisfying === null)
const highestVersion = [...new Set(versions.flat())].sort(semver.rcompare);
if (highestVersion.length === 0)
return null;

return {name: finalDescriptor.name, reference: maxSatisfying};
return {name: finalDescriptor.name, reference: highestVersion[0]};
}

private getLastKnownGoodFile() {
Expand Down
64 changes: 64 additions & 0 deletions sources/commands/Base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {Command, UsageError} from 'clipanion';
import fs from 'fs';

import {PreparedPackageManagerInfo} from '../Engine';
import * as corepackUtils from '../corepackUtils';
import {Context} from '../main';
import * as nodeUtils from '../nodeUtils';
import * as specUtils from '../specUtils';

export abstract class BaseCommand extends Command<Context> {
async resolvePatternsToDescriptors({all, patterns}: {all: boolean, patterns: Array<string>}) {
if (all && patterns.length > 0)
throw new UsageError(`The --all option cannot be used along with an explicit package manager specification`);

const resolvedSpecs = all
? await this.context.engine.getDefaultDescriptors()
: patterns.map(pattern => specUtils.parseSpec(pattern, `CLI arguments`, {enforceExactVersion: false}));

if (resolvedSpecs.length === 0) {
const lookup = await specUtils.loadSpec(this.context.cwd);
switch (lookup.type) {
case `NoProject`:
throw new UsageError(`Couldn't find a project in the local directory - please explicit the package manager to pack, or run this command from a valid project`);

case `NoSpec`:
throw new UsageError(`The local project doesn't feature a 'packageManager' field - please explicit the package manager to pack, or update the manifest to reference it`);

default: {
return [lookup.spec];
}
}
}

return resolvedSpecs;
}

async setLocalPackageManager(info: PreparedPackageManagerInfo) {
const lookup = await specUtils.loadSpec(this.context.cwd);

const content = lookup.target !== `NoProject`
? await fs.promises.readFile(lookup.target, `utf8`)
: ``;

const {data, indent} = nodeUtils.readPackageJson(content);

const previousPackageManager = data.packageManager ?? `unknown`;
data.packageManager = `${info.locator.name}@${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)
return 0;

// Adding it into the environment avoids breaking package managers that
// don't expect those options.
process.env.COREPACK_MIGRATE_FROM = previousPackageManager;
this.context.stdout.write(`\n`);

const [binaryName, ...args] = command;
return await corepackUtils.runVersion(info.locator, info, binaryName, args);
}
}
Loading

0 comments on commit fe3e5cd

Please sign in to comment.