@salesforce/plugin-release-management
Version:
A plugin for preparing and publishing npm packages
263 lines • 11.7 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 fs from 'node:fs';
import path from 'node:path';
import semver from 'semver';
import { ux } from '@oclif/core';
import shelljs from 'shelljs';
import { SfError } from '@salesforce/core';
import { AsyncOptionalCreatable, findKey, parseJson } from '@salesforce/kit';
import { isObject, isPlainObject } from '@salesforce/ts-types';
import { Registry } from './registry.js';
export function parsePackageVersion(alias) {
const regex = /[0-9]{1,3}(?:.[0-9]{1,3})?(?:.[0-9]{1,3})?(.*?)$/;
return regex.exec(alias)?.[0];
}
export class Package extends AsyncOptionalCreatable {
// three props set during init
name;
npmPackage;
packageJson;
location;
registry;
constructor(opts) {
super();
this.location = opts?.location ?? shelljs.pwd().stdout;
this.registry = new Registry();
}
async readPackageJson() {
const pkgJsonPath = this.location ? path.join(this.location, 'package.json') : 'package.json';
const fileData = await fs.promises.readFile(pkgJsonPath, 'utf8');
return parseJson(fileData, pkgJsonPath, false);
}
/**
* Retrieve the npm package info using `npm view`
*
* It'll first try to find the package with the version listed in the package.json
* If that version doesn't exist, it'll find the version tagged as latest
*/
retrieveNpmPackage() {
let result = shelljs.exec(`npm view ${this.name}@${this.packageJson.version} ${this.registry.getRegistryParameter()} --json`, { silent: true });
if (!result.stdout) {
result = shelljs.exec(`npm view ${this.name} ${this.registry.getRegistryParameter()} --json`, { silent: true });
}
if (result.stdout) {
return JSON.parse(result.stdout);
}
}
nextVersionIsAvailable(nextVersion) {
const pkg = this.retrieveNpmPackage();
return pkg?.versions?.includes(nextVersion) ?? false;
}
writePackageJson(rootDir) {
const pkgJsonPath = rootDir ? path.join(rootDir, 'package.json') : 'package.json';
const fileData = JSON.stringify(this.packageJson, null, 2);
fs.writeFileSync(pkgJsonPath, fileData, {
encoding: 'utf8',
mode: '600',
});
}
calculatePinnedPackageUpdates(pinnedPackages) {
return pinnedPackages.map((dep) => {
const versions = this.getDistTags(dep.name);
let tag = dep.tag;
// if tag is 'latest-rc' and there's no latest-rc release for a package, default to latest
if (!versions[tag]) {
tag = 'latest';
}
// If the version in package.json is greater than the version of the requested tag, then we
// assume that this is on purpose - so we don't overwrite it. For example, we might want to
// include a latest-rc version for a single plugin but everything else we want latest.
let version;
if (semver.gt(dep.version, versions[tag])) {
ux.warn(`${dep.name} is currently pinned at ${dep.version} which is higher than ${tag} (${versions[tag]}). Assuming that this is intentional...`);
version = dep.version;
const matchedTag = findKey(versions, (v) => v === version);
tag = matchedTag ?? tag;
}
else {
version = versions[tag];
}
return { name: dep.name, version, tag };
});
}
getDistTags(name) {
const result = shelljs.exec(`npm view ${name} dist-tags ${this.registry.getRegistryParameter()} --json`, {
silent: true,
});
if (result.stdout) {
return JSON.parse(result.stdout);
}
ux.error(result.stderr);
}
bumpResolutions(tag) {
if (!this.packageJson.resolutions) {
throw new SfError('Bumping resolutions requires property "resolutions" to be present in package.json');
}
return Object.fromEntries(Object.entries(this.packageJson.resolutions).map(([key]) => {
const versions = this.getDistTags(key);
return [key, versions[tag]];
}));
}
/**
* Lookup dependency info by package name
* Examples: @salesforce/plugin-info
* Pass in the dependencies you want to search through (dependencies, devDependencies, resolutions, etc)
*/
// eslint-disable-next-line class-methods-use-this
getDependencyInfo(name, dependencies) {
const match = Object.entries(dependencies).find(([key]) => key === name);
if (match) {
const [matchingName, value] = match;
return {
packageName: matchingName,
currentVersion: value,
};
}
else {
ux.error(`${name} was not found in the dependencies section of the package.json`);
}
}
bumpDependencyVersions(targetDependencies) {
return targetDependencies
.map((dep) => {
// regex for npm package with optional namespace and version
// https://regex101.com/r/HmIu3N/1
const npmPackageRegex = /^((?:@[^/]+\/)?[^@/]+)(?:@([^@/]+))?$/;
const [, name, version] = npmPackageRegex.exec(dep) ?? [];
// We will look for packages in dependencies and resolutions
const { dependencies, resolutions } = this.packageJson;
const jitPlugins = this.packageJson.oclif?.jitPlugins ?? {};
// find dependency in package.json (could be an npm alias)
const depInfo = this.getDependencyInfo(name, { ...dependencies, ...resolutions, ...jitPlugins });
const shouldPin = this.shouldPinDependency(depInfo.packageName);
// if a version is not provided, we'll look up the "latest" version
depInfo.finalVersion = `${shouldPin ? '' : '^'}${version ?? this.getDistTags(depInfo.packageName).latest}`;
// return if version did not change
if (depInfo.currentVersion === depInfo.finalVersion)
return;
// update dependency (or resolution) in package.json
if (dependencies[depInfo.packageName]) {
this.packageJson.dependencies[depInfo.packageName] = depInfo.finalVersion;
}
else if (resolutions?.[depInfo.packageName] && this.packageJson.resolutions) {
this.packageJson.resolutions[depInfo.packageName] = depInfo.finalVersion;
}
else if (this.packageJson.oclif?.jitPlugins) {
this.packageJson.oclif.jitPlugins[depInfo.packageName] = depInfo.finalVersion;
}
return depInfo;
})
.filter((item) => Boolean(item)); // remove falsy values, in this case the `undefined` if version did not change
}
determineNextVersion(isPatch = false, prerelease) {
const currentVersion = this.packageJson.version;
const releaseType = prerelease ? 'prerelease' : isPatch ? 'patch' : 'minor';
const result = semver.inc(currentVersion, releaseType, prerelease);
if (!result) {
throw new SfError(`Unable to determine next version from ${currentVersion} and ${releaseType}. semver.inc returned null (invalid version)`);
}
return result;
}
pinDependencyVersions(targetTag) {
// get the list of dependencies to hardcode
if (!this.packageJson.pinnedDependencies) {
throw new SfError('Pinning package dependencies requires property "pinnedDependencies" to be present in package.json');
}
const { pinnedDependencies, dependencies } = this.packageJson;
const deps = pinnedDependencies
.map((d) => {
const [name, tag] = getNameAndTag(d);
if (!dependencies[name]) {
ux.warn(`${name} was not found in the dependencies section of your package.json. Skipping...`);
return;
}
const version = dependencies[name];
return getPinnedPackage({ name, version, tag, targetTag });
})
.filter((pp) => isPlainObject(pp));
const updatedDeps = this.calculatePinnedPackageUpdates(deps);
updatedDeps.forEach((pp) => {
this.packageJson.dependencies[pp.name] = pp.version;
});
return updatedDeps;
}
bumpJit(targetTag = 'latest-rc') {
const { pinnedDependencies, dependencies, devDependencies, oclif } = this.packageJson;
// no JIT is ok
if (!oclif?.jitPlugins) {
return;
}
const jitDeps = Object.entries(oclif.jitPlugins)
.map(([plugin, version]) => {
const [name, tag] = getNameAndTag(plugin);
if (dependencies?.[name] ??
devDependencies?.[name] ??
pinnedDependencies?.includes(name) ??
oclif.plugins?.includes(name) ??
oclif.devPlugins?.includes(name)) {
throw new SfError('JIT plugins should not be listed in dependencies, devDependencies, pinnedDependencies, oclif.plugins or oclif.devPlugins. ');
}
return getPinnedPackage({ name, version, tag, targetTag });
})
.filter(isObject);
const updatedDeps = this.calculatePinnedPackageUpdates(jitDeps);
// side effect: mutate package.json reference
updatedDeps.forEach((pp) => {
if (this.packageJson.oclif?.jitPlugins) {
if (this.packageJson.oclif?.jitPlugins) {
this.packageJson.oclif.jitPlugins[pp.name] = pp.version;
}
}
});
return updatedDeps;
}
/**
* Returns true if the version specified in the package.json has not been
* published to the registry
*/
nextVersionIsHardcoded() {
return !(this.npmPackage.versions ?? []).includes(this.packageJson.version);
}
hasScript(scriptName) {
return typeof this.packageJson.scripts[scriptName] === 'string';
}
async init() {
this.packageJson = await this.readPackageJson();
this.name = this.packageJson.name;
this.npmPackage = this.retrieveNpmPackage() ?? this.createDefaultNpmPackage();
}
createDefaultNpmPackage() {
return {
name: this.name,
version: this.packageJson.version,
versions: [],
'dist-tags': {},
};
}
shouldPinDependency(dependencyName) {
const pinnedDependencies = this.packageJson.pinnedDependencies ?? [];
const jitDependencies = this.packageJson.oclif?.jitPlugins
? Object.keys(this.packageJson.oclif.jitPlugins)
: [];
const dependenciesThatShouldBePinned = [...pinnedDependencies, ...jitDependencies];
return dependenciesThatShouldBePinned.includes(dependencyName);
}
}
const getNameAndTag = (plugin) => {
const tagRegex = /(?<=(^@.*?)@)(.*?)$/;
const [tag] = tagRegex.exec(plugin) ?? [];
const name = tag ? plugin.replace(new RegExp(`@${tag}$`), '') : plugin;
return [name, tag];
};
/** standardize various plugin formats/targets to a PinnedPackage */
const getPinnedPackage = ({ name, version, tag, targetTag, }) => ({
name,
version: version.split('@').reverse()[0].replace('^', '').replace('~', ''),
tag: tag ?? targetTag,
});
//# sourceMappingURL=package.js.map