Skip to content

Commit fe3e5cd

Browse files
authored
Refactoring of the CLI interface (#291)
* 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
1 parent b8a4a52 commit fe3e5cd

36 files changed

+751
-180
lines changed

README.md

+46-24
Original file line numberDiff line numberDiff line change
@@ -92,26 +92,25 @@ If there is no Known Good Release for the requested package manager, Corepack
9292
looks up the npm registry for the latest available version and cache it for
9393
future use.
9494

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

9897
## Offline Workflow
9998

10099
The utility commands detailed in the next section.
101100

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

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

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

116115
## Utility Commands
117116

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

174-
### `corepack prepare [... name@version]`
173+
### `corepack install`
175174

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

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

188-
### `corepack hydrate <path/to/corepack.tgz>`
182+
| Option | Description |
183+
| --------------------- | ------------------------------------------ |
184+
| `--all` | Install all Last Known Good releases |
185+
186+
Install the selected package managers and install them on the system.
187+
188+
Package managers thus installed will be configured as the new default when
189+
calling their respective binaries outside of projects defining the
190+
`packageManager` field.
191+
192+
### `corepack pack [--all] [... name@version]`
193+
194+
| Option | Description |
195+
| --------------------- | ------------------------------------------ |
196+
| `--all` | Pack all Last Known Good releases |
197+
| `--json ` | Print the output folder rather than logs |
198+
| `-o,--output ` | Path where to generate the archive |
199+
200+
Download the selected package managers and store them inside a tarball
201+
suitable for use with `corepack install -g`.
202+
203+
### `corepack use <name@version>`
204+
205+
When run, this command will retrieve the latest release matching the provided
206+
descriptor, assign it to the project's package.json file, and automatically
207+
perform an install.
208+
209+
### `corepack up`
189210

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

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

198220
## Environment Variables
199221

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

209231
- `COREPACK_ENABLE_STRICT` can be set to `0` to prevent Corepack from throwing
210232
error if the package manager does not correspond to the one defined for the

config.json

+15
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
"registry": {
2828
"type": "npm",
2929
"package": "npm"
30+
},
31+
"commands": {
32+
"use": ["npm", "install"]
3033
}
3134
}
3235
}
@@ -62,6 +65,9 @@
6265
"registry": {
6366
"type": "npm",
6467
"package": "pnpm"
68+
},
69+
"commands": {
70+
"use": ["pnpm", "install"]
6571
}
6672
},
6773
">=6.0.0": {
@@ -73,6 +79,9 @@
7379
"registry": {
7480
"type": "npm",
7581
"package": "pnpm"
82+
},
83+
"commands": {
84+
"use": ["pnpm", "install"]
7685
}
7786
}
7887
}
@@ -102,6 +111,9 @@
102111
"registry": {
103112
"type": "npm",
104113
"package": "yarn"
114+
},
115+
"commands": {
116+
"use": ["yarn", "install"]
105117
}
106118
},
107119
">=2.0.0": {
@@ -118,6 +130,9 @@
118130
"tags": "latest",
119131
"versions": "tags"
120132
}
133+
},
134+
"commands": {
135+
"use": ["yarn", "install"]
121136
}
122137
}
123138
}

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@
2424
"@jest/globals": "^29.0.0",
2525
"@types/debug": "^4.1.5",
2626
"@types/jest": "^29.0.0",
27-
"@types/node": "^20.0.0",
27+
"@types/node": "^20.4.6",
2828
"@types/semver": "^7.1.0",
2929
"@types/tar": "^6.0.0",
3030
"@types/which": "^3.0.0",
3131
"@typescript-eslint/eslint-plugin": "^5.0.0",
3232
"@typescript-eslint/parser": "^5.0.0",
3333
"@yarnpkg/eslint-config": "^0.6.0-rc.7",
34-
"@yarnpkg/fslib": "^2.1.0",
34+
"@yarnpkg/fslib": "^3.0.0-rc.48",
3535
"@zkochan/cmd-shim": "^6.0.0",
3636
"babel-plugin-dynamic-import-node": "^2.3.3",
3737
"clipanion": "^3.0.1",

sources/Engine.ts

+31-31
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import * as semverUtils from './semverUtil
1212
import {Config, Descriptor, Locator} from './types';
1313
import {SupportedPackageManagers, SupportedPackageManagerSet} from './types';
1414

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

1617
export class Engine {
1718
constructor(public config: Config = defaultConfig as Config) {
@@ -33,6 +34,19 @@ export class Engine {
3334
return null;
3435
}
3536

37+
getPackageManagerSpecFor(locator: Locator) {
38+
const definition = this.config.definitions[locator.name];
39+
if (typeof definition === `undefined`)
40+
throw new UsageError(`This package manager (${locator.name}) isn't supported by this corepack build`);
41+
42+
const ranges = Object.keys(definition.ranges).reverse();
43+
const range = ranges.find(range => semverUtils.satisfiesWithPrereleases(locator.reference, range));
44+
if (typeof range === `undefined`)
45+
throw new Error(`Assertion failed: Specified resolution (${locator.reference}) isn't supported by any of ${ranges.join(`, `)}`);
46+
47+
return definition.ranges[range];
48+
}
49+
3650
getBinariesFor(name: SupportedPackageManagers) {
3751
const binNames = new Set<string>();
3852

@@ -111,25 +125,23 @@ export class Engine {
111125
}
112126

113127
async ensurePackageManager(locator: Locator) {
114-
const definition = this.config.definitions[locator.name];
115-
if (typeof definition === `undefined`)
116-
throw new UsageError(`This package manager (${locator.name}) isn't supported by this corepack build`);
117-
118-
const ranges = Object.keys(definition.ranges).reverse();
119-
const range = ranges.find(range => semverUtils.satisfiesWithPrereleases(locator.reference, range));
120-
if (typeof range === `undefined`)
121-
throw new Error(`Assertion failed: Specified resolution (${locator.reference}) isn't supported by any of ${ranges.join(`, `)}`);
128+
const spec = this.getPackageManagerSpecFor(locator);
122129

123-
const installedLocation = await corepackUtils.installVersion(folderUtils.getInstallFolder(), locator, {
124-
spec: definition.ranges[range],
130+
const packageManagerInfo = await corepackUtils.installVersion(folderUtils.getInstallFolder(), locator, {
131+
spec,
125132
});
126133

127134
return {
128-
location: installedLocation,
129-
spec: definition.ranges[range],
135+
...packageManagerInfo,
136+
locator,
137+
spec,
130138
};
131139
}
132140

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

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

168-
const candidateRangeDefinitions = Object.keys(definition.ranges).filter(range => {
169-
return semverUtils.satisfiesWithPrereleases(finalDescriptor.range, range);
170-
});
171-
172-
const tagResolutions = await Promise.all(candidateRangeDefinitions.map(async range => {
173-
return [range, await corepackUtils.fetchAvailableVersions(definition.ranges[range].registry)] as const;
180+
const versions = await Promise.all(Object.keys(definition.ranges).map(async range => {
181+
const versions = await corepackUtils.fetchAvailableVersions(definition.ranges[range].registry);
182+
return versions.filter(version => semverUtils.satisfiesWithPrereleases(version, finalDescriptor.range));
174183
}));
175184

176-
// If a version is available under multiple strategies (for example if
177-
// Yarn is published to both the v1 package and git), we only care
178-
// about the latest one
179-
const resolutionMap = new Map();
180-
for (const [range, resolutions] of tagResolutions)
181-
for (const entry of resolutions)
182-
resolutionMap.set(entry, range);
183-
184-
const candidates = [...resolutionMap.keys()];
185-
const maxSatisfying = semver.maxSatisfying(candidates, finalDescriptor.range);
186-
if (maxSatisfying === null)
185+
const highestVersion = [...new Set(versions.flat())].sort(semver.rcompare);
186+
if (highestVersion.length === 0)
187187
return null;
188188

189-
return {name: finalDescriptor.name, reference: maxSatisfying};
189+
return {name: finalDescriptor.name, reference: highestVersion[0]};
190190
}
191191

192192
private getLastKnownGoodFile() {

sources/commands/Base.ts

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {Command, UsageError} from 'clipanion';
2+
import fs from 'fs';
3+
4+
import {PreparedPackageManagerInfo} from '../Engine';
5+
import * as corepackUtils from '../corepackUtils';
6+
import {Context} from '../main';
7+
import * as nodeUtils from '../nodeUtils';
8+
import * as specUtils from '../specUtils';
9+
10+
export abstract class BaseCommand extends Command<Context> {
11+
async resolvePatternsToDescriptors({all, patterns}: {all: boolean, patterns: Array<string>}) {
12+
if (all && patterns.length > 0)
13+
throw new UsageError(`The --all option cannot be used along with an explicit package manager specification`);
14+
15+
const resolvedSpecs = all
16+
? await this.context.engine.getDefaultDescriptors()
17+
: patterns.map(pattern => specUtils.parseSpec(pattern, `CLI arguments`, {enforceExactVersion: false}));
18+
19+
if (resolvedSpecs.length === 0) {
20+
const lookup = await specUtils.loadSpec(this.context.cwd);
21+
switch (lookup.type) {
22+
case `NoProject`:
23+
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`);
24+
25+
case `NoSpec`:
26+
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`);
27+
28+
default: {
29+
return [lookup.spec];
30+
}
31+
}
32+
}
33+
34+
return resolvedSpecs;
35+
}
36+
37+
async setLocalPackageManager(info: PreparedPackageManagerInfo) {
38+
const lookup = await specUtils.loadSpec(this.context.cwd);
39+
40+
const content = lookup.target !== `NoProject`
41+
? await fs.promises.readFile(lookup.target, `utf8`)
42+
: ``;
43+
44+
const {data, indent} = nodeUtils.readPackageJson(content);
45+
46+
const previousPackageManager = data.packageManager ?? `unknown`;
47+
data.packageManager = `${info.locator.name}@${info.locator.reference}+${info.hash}`;
48+
49+
const newContent = nodeUtils.normalizeLineEndings(content, `${JSON.stringify(data, null, indent)}\n`);
50+
await fs.promises.writeFile(lookup.target, newContent, `utf8`);
51+
52+
const command = this.context.engine.getPackageManagerSpecFor(info.locator).commands?.use ?? null;
53+
if (command === null)
54+
return 0;
55+
56+
// Adding it into the environment avoids breaking package managers that
57+
// don't expect those options.
58+
process.env.COREPACK_MIGRATE_FROM = previousPackageManager;
59+
this.context.stdout.write(`\n`);
60+
61+
const [binaryName, ...args] = command;
62+
return await corepackUtils.runVersion(info.locator, info, binaryName, args);
63+
}
64+
}

0 commit comments

Comments
 (0)