@salesforce/plugin-release-management
Version:
A plugin for preparing and publishing npm packages
166 lines • 6.21 kB
JavaScript
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import os from 'node:os';
import { Ux } from '@salesforce/sf-plugins-core';
import shelljs from 'shelljs';
import { Logger, SfError } from '@salesforce/core';
import { AsyncOptionalCreatable, Env, sleep } from '@salesforce/kit';
import chalk from 'chalk';
import { isString } from '@salesforce/ts-types';
import { Package } from './package.js';
import { Registry } from './registry.js';
import { api as packAndSignApi } from './codeSigning/packAndSign.js';
class Repository extends AsyncOptionalCreatable {
options;
ux;
env;
registry;
stepCounter = 1;
constructor(options) {
super(options);
this.options = options;
this.ux = options?.ux ?? new Ux();
this.env = new Env();
this.registry = new Registry();
}
install(silent = false) {
this.execCommand(`yarn install ${this.registry.getRegistryParameter()}`, silent);
}
build(silent = false) {
this.execCommand('yarn build', silent);
}
run(script, location, silent = false) {
if (location) {
this.execCommand(`(cd ${location} && yarn run ${script})`, silent);
}
else {
this.execCommand(`(yarn run ${script})`, silent);
}
}
test() {
this.execCommand('yarn test');
}
printStage(msg) {
this.ux.log(chalk.green.bold(`${os.EOL}${this.stepCounter}) ${msg}`));
this.stepCounter += 1;
}
async writeNpmToken() {
const home = this.env.getString('HOME') ?? os.homedir();
await this.registry.setNpmAuth(home);
await this.registry.setNpmRegistry(home);
}
execCommand(cmd, silent) {
if (!silent)
this.ux.log(`${chalk.dim(cmd)}${os.EOL}`);
const result = shelljs.exec(cmd, { silent });
if (result.code !== 0) {
throw new SfError(result.stderr, 'FailedCommandExecution');
}
else {
return result;
}
}
async poll(checkFn) {
const isNonTTY = this.env.getBoolean('CI') || this.env.getBoolean('CIRCLECI');
let found = false;
let attempts = 0;
const maxAttempts = 300;
const start = isNonTTY
? (msg) => this.ux.log(msg)
: (msg) => this.ux.spinner.start(msg);
const update = isNonTTY
? (msg) => this.ux.log(msg)
: (msg) => (this.ux.spinner.status = msg);
const stop = isNonTTY ? (msg) => this.ux.log(msg) : (msg) => this.ux.spinner.stop(msg);
start('Polling for new version(s) to become available on npm');
while (attempts < maxAttempts && !found) {
attempts += 1;
update(`attempt: ${attempts} of ${maxAttempts}`);
found = checkFn();
// eslint-disable-next-line no-await-in-loop
await sleep(1000);
}
stop(attempts >= maxAttempts ? 'failed' : 'done');
return found;
}
}
export class PackageRepo extends Repository {
// all props are set in init(), so ! is safe
name;
nextVersion;
package;
// Both loggers are used because some logs we only want to show in the debug output
// but other logs we always want to go to stdout
logger;
constructor(options) {
super(options);
}
async sign() {
packAndSignApi.setUx(this.ux);
return packAndSignApi.packSignVerifyModifyPackageJSON(this.package.location);
}
// eslint-disable-next-line class-methods-use-this
async revertChanges() {
return packAndSignApi.revertPackageJsonIfExists();
}
getPkgInfo() {
return {
name: this.name,
nextVersion: this.nextVersion,
registryParam: this.registry.getRegistryParameter(),
};
}
async publish(opts = {}) {
const { dryrun, signatures, access, tag } = opts;
if (!dryrun)
await this.writeNpmToken();
let cmd = 'npm publish';
if (signatures?.[0]?.fileTarPath)
cmd += ` ${signatures[0]?.fileTarPath}`;
if (tag)
cmd += ` --tag ${tag}`;
if (dryrun)
cmd += ' --dry-run';
cmd += ` ${this.registry.getRegistryParameter()}`;
cmd += ` --access ${access ?? 'public'}`;
this.execCommand(cmd);
}
async waitForAvailability() {
return this.poll(() => this.package.nextVersionIsAvailable(this.nextVersion));
}
getSuccessMessage() {
return chalk.green.bold(`Successfully released ${this.name}@${this.nextVersion}`);
}
async init() {
this.logger = await Logger.child(this.constructor.name);
this.package = await Package.create({ location: undefined });
this.nextVersion = this.determineNextVersion();
this.name = this.package.npmPackage.name;
}
determineNextVersion() {
if (this.package.nextVersionIsHardcoded()) {
this.logger.debug(`${this.package.packageJson.name}@${this.package.packageJson.version} does not exist in the registry. Assuming that it's the version we want published`);
return this.package.packageJson.version;
}
else {
this.logger.debug('Using commit-and-tag-version to determine next version');
let command = 'npx commit-and-tag-version --dry-run --skip.tag --skip.commit --skip.changelog';
// It can be an empty string if they want
if (isString(this.options?.useprerelease)) {
command += ` --prerelease ${this.options?.useprerelease}`;
}
const result = this.execCommand(command, true);
const nextVersionRegex = /(?<=to\s)([0-9]{1,}\.|.){2,}/gi;
const nextVersion = result.match(nextVersionRegex)?.[0];
if (!nextVersion) {
throw new SfError(`Could not determine next version from ${result} using regex`);
}
return nextVersion;
}
}
}
//# sourceMappingURL=repository.js.map