UNPKG

@salesforce/plugin-release-management

Version:
263 lines 11.7 kB
/* * 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