mirror of https://github.com/nodejs/corepack.git
156 lines
5.3 KiB
TypeScript
156 lines
5.3 KiB
TypeScript
import fs from 'fs';
|
|
import path from 'path';
|
|
import semver from 'semver';
|
|
import { UsageError } from 'clipanion';
|
|
import Enquirer from 'enquirer';
|
|
|
|
import * as miscUtils from './miscUtils';
|
|
import {SupportedPackageManagers, SupportedPackageManagerSet, Descriptor, Locator} from './types';
|
|
|
|
/**
|
|
* Locates the active project's package manager specification.
|
|
*
|
|
* If the specification exists but doesn't match the active package manager,
|
|
* an error is thrown to prevent users from using the wrong package manager,
|
|
* which would lead to inconsistent project layouts.
|
|
*
|
|
* If the project doesn't include a specification file, we just assume that
|
|
* whatever the user uses is exactly what they want to use. Since the version
|
|
* isn't explicited, we fallback on known good versions.
|
|
*
|
|
* Finally, if the project doesn't exist at all, we ask the user whether they
|
|
* want to create one in the current project. If they do, we initialize a new
|
|
* project using the default package managers, and configure it so that we
|
|
* don't need to ask again in the future.
|
|
*/
|
|
export async function findProjectSpec(initialCwd: string, locator: Locator): Promise<Descriptor> {
|
|
while (true) {
|
|
const result = await loadSpec(initialCwd);
|
|
|
|
switch (result.type) {
|
|
case `NoProject`: {
|
|
await initProjectAndSpec(result.target, locator);
|
|
} break;
|
|
|
|
case `NoSpec`: {
|
|
// A locator is a valid descriptor (but not the other way around)
|
|
return {name: locator.name, range: locator.reference};
|
|
} break;
|
|
|
|
case `Found`: {
|
|
if (result.spec.name !== locator.name) {
|
|
throw new UsageError(`This project is configured to use ${result.spec.name}`);
|
|
} else {
|
|
return result.spec;
|
|
}
|
|
} break;
|
|
}
|
|
}
|
|
}
|
|
|
|
type LoadSpecResult =
|
|
| {type: `NoProject`, target: string}
|
|
| {type: `NoSpec`, target: string}
|
|
| {type: `Found`, spec: Descriptor};
|
|
|
|
async function loadSpec(initialCwd: string): Promise<LoadSpecResult> {
|
|
let nextCwd = initialCwd;
|
|
let currCwd = ``;
|
|
|
|
let selection: any = null;
|
|
|
|
while (nextCwd !== currCwd && selection === null) {
|
|
currCwd = nextCwd;
|
|
nextCwd = path.dirname(currCwd);
|
|
|
|
const manifestPath = path.join(currCwd, `package.json`);
|
|
if (!fs.existsSync(manifestPath))
|
|
continue;
|
|
|
|
const content = await fs.promises.readFile(manifestPath, `utf8`);
|
|
|
|
let data;
|
|
try {
|
|
data = JSON.parse(content);
|
|
} catch {}
|
|
|
|
if (typeof data !== `object` || data === null)
|
|
throw new UsageError(`Invalid package.json in ${path.relative(initialCwd, manifestPath)}`);
|
|
|
|
selection = {data, manifestPath};
|
|
}
|
|
|
|
if (selection === null)
|
|
return {type: `NoProject`, target: path.join(initialCwd, `package.json`)};
|
|
|
|
const engines = selection.data.engines;
|
|
if (typeof engines === `undefined`)
|
|
return {type: `NoSpec`, target: selection.manifestPath};
|
|
if (typeof engines !== `object` || engines === null)
|
|
throw new UsageError(`Invalid 'engines' field in ${path.relative(initialCwd, selection.manifestPath)}; expected an object`);
|
|
|
|
const pmSpec = engines.pm;
|
|
if (typeof pmSpec === `undefined`)
|
|
return {type: `NoSpec`, target: selection.manifestPath};
|
|
if (typeof pmSpec !== `string`)
|
|
throw new UsageError(`Invalid package manager specification in ${path.relative(initialCwd, selection.manifestPath)}; expected a semver range`);
|
|
|
|
const match = pmSpec.match(/^(?!_)(.+)@(.+)$/);
|
|
if (match === null || !semver.validRange(match[2]))
|
|
throw new UsageError(`Invalid package manager specification in ${path.relative(initialCwd, selection.manifestPath)}; expected a semver range`);
|
|
|
|
if (!SupportedPackageManagerSet.has(match[1]))
|
|
throw new UsageError(`Unsupported package manager specification (${match})`);
|
|
|
|
return {
|
|
type: `Found`,
|
|
spec: {
|
|
name: match[1] as SupportedPackageManagers,
|
|
range: match[2],
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function persistPmSpec(updateTarget: string, locator: Locator, message: string) {
|
|
const newSpec = `${locator.name}@^${locator.reference}`;
|
|
|
|
let res: boolean;
|
|
try {
|
|
res = await Enquirer.prompt([{
|
|
type: `confirm`,
|
|
name: `confirm`,
|
|
initial: true,
|
|
message: message.replace(`{}`, newSpec),
|
|
}]);
|
|
} catch (err) {
|
|
if (err === ``) {
|
|
res = false;
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
if (!res)
|
|
throw new miscUtils.Cancellation();
|
|
|
|
const content = fs.existsSync(updateTarget)
|
|
? await fs.promises.readFile(updateTarget, `utf8`)
|
|
: `{}`;
|
|
|
|
const data = JSON.parse(content);
|
|
|
|
data.engines = data.engines || {};
|
|
data.engines.pm = newSpec;
|
|
|
|
const serialized = JSON.stringify(data, null, 2);
|
|
await fs.promises.writeFile(updateTarget, `${serialized}\n`);
|
|
}
|
|
|
|
export async function initProjectAndSpec(updateTarget: string, locator: Locator) {
|
|
return await persistPmSpec(updateTarget, locator, `No configured project yet; set it to {}?`);
|
|
}
|
|
|
|
export async function initSpec(updateTarget: string, locator: Locator) {
|
|
return await persistPmSpec(updateTarget, locator, `No configured local package manager yet; set it to {}?`);
|
|
}
|