Skip to content

Commit 739afa9

Browse files
authored
Merge pull request #496 from salesforcecli/ew/build-rc-only-flag
`--only` flag for the build-latest-rc command
2 parents 2e234a2 + 7072d2e commit 739afa9

File tree

5 files changed

+288
-27
lines changed

5 files changed

+288
-27
lines changed

command-snapshot.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
{
4242
"command": "cli:latestrc:build",
4343
"plugin": "@salesforce/plugin-release-management",
44-
"flags": ["build-only", "json", "loglevel", "patch", "pinned-deps", "rctag", "resolutions"]
44+
"flags": ["build-only", "json", "loglevel", "only", "patch", "pinned-deps", "rctag", "resolutions"]
4545
},
4646
{
4747
"command": "cli:releasenotes",

messages/cli.latestrc.build.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
{
22
"description": "creates a PR to the repository property defined in the package.json to release a latest-rc build",
3+
"examples": [
4+
"<%= config.bin %> <%= command.id %>",
5+
"<%= config.bin %> <%= command.id %> --patch",
6+
"<%= config.bin %> <%= command.id %> --build-only",
7+
"<%= config.bin %> <%= command.id %> --only @salesforce/plugin-source,@salesforce/plugin-info@1.2.3,@sf/config"
8+
],
39
"flags": {
410
"rctag": "the tag name that corresponds to the npm RC build, usually latest-rc or stable-rc",
511
"resolutions": "bump the versions of packages listed in the resolutions section",
612
"pinnedDeps": "bump the versions of the packages listed in the pinnedDependencies section",
13+
"only": "only bump the version of the packages passed in, uses latest if version is not provided",
714
"patch": "bump the release as a patch of an existing version, not a new minor version",
815
"buildOnly": "only build the latest rc, do not git add/commit/push"
916
}

src/commands/cli/latestrc/build.ts

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,23 @@
44
* Licensed under the BSD 3-Clause license.
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
7+
8+
import * as os from 'os';
79
import { flags, FlagsConfig, SfdxCommand } from '@salesforce/command';
810
import { exec, ExecOptions } from 'shelljs';
911
import { ensureString } from '@salesforce/ts-types';
1012
import { Env } from '@salesforce/kit';
1113
import { Octokit } from '@octokit/core';
1214
import { bold } from 'chalk';
13-
import { Messages } from '@salesforce/core';
15+
import { Messages, SfdxError } from '@salesforce/core';
1416
import { SinglePackageRepo } from '../../../repository';
1517

1618
Messages.importMessagesDirectory(__dirname);
1719
const messages = Messages.loadMessages('@salesforce/plugin-release-management', 'cli.latestrc.build');
1820

1921
export default class build extends SfdxCommand {
2022
public static readonly description = messages.getMessage('description');
23+
public static readonly examples = messages.getMessage('examples').split(os.EOL);
2124
public static readonly flagsConfig: FlagsConfig = {
2225
rctag: flags.string({
2326
description: messages.getMessage('flags.rctag'),
@@ -32,6 +35,9 @@ export default class build extends SfdxCommand {
3235
default: true,
3336
allowNo: true,
3437
}),
38+
only: flags.array({
39+
description: messages.getMessage('flags.only'),
40+
}),
3541
'pinned-deps': flags.boolean({
3642
description: messages.getMessage('flags.pinnedDeps'),
3743
default: true,
@@ -72,18 +78,30 @@ export default class build extends SfdxCommand {
7278
repo.package.setNextVersion(nextRCVersion);
7379
repo.package.packageJson.version = nextRCVersion;
7480

75-
// bump resolution deps
76-
if (this.flags.resolutions) {
77-
this.ux.log('bumping resolutions in the package.json to their "latest"');
78-
repo.package.bumpResolutions('latest');
81+
const only = this.flags.only as string[];
82+
83+
if (only) {
84+
this.ux.log(`bumping the following dependencies only: ${only.join(', ')}`);
85+
const bumped = repo.package.bumpDependencyVersions(only);
86+
87+
if (!bumped.length) {
88+
throw new SfdxError(
89+
'No version changes made. Confirm you are passing the correct dependency and version to --only.'
90+
);
91+
}
92+
} else {
93+
// bump resolution deps
94+
if (this.flags.resolutions) {
95+
this.ux.log('bumping resolutions in the package.json to their "latest"');
96+
repo.package.bumpResolutions('latest');
97+
}
98+
99+
// pin the pinned dependencies
100+
if (this.flags['pinned-deps']) {
101+
this.ux.log('pinning dependencies in pinnedDependencies to "latest-rc"');
102+
repo.package.pinDependencyVersions('latest-rc');
103+
}
79104
}
80-
81-
// pin the pinned dependencies
82-
if (this.flags['pinned-deps']) {
83-
this.ux.log('pinning dependencies in pinnedDependencies to "latest-rc"');
84-
repo.package.pinDependencyVersions('latest-rc');
85-
}
86-
87105
repo.package.writePackageJson();
88106

89107
this.exec('yarn install');

src/package.ts

Lines changed: 100 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,15 @@ interface PinnedPackage {
6363
alias: Nullable<string>;
6464
}
6565

66+
// Differentiates between dependencyName and packageName to support npm aliases
67+
interface DependencyInfo {
68+
dependencyName: string;
69+
packageName: string;
70+
alias: Nullable<string>;
71+
currentVersion?: string;
72+
finalVersion?: string;
73+
}
74+
6675
export function parseAliasedPackageName(alias: string): string {
6776
return alias.replace('npm:', '').replace(/@(\^|~)?[0-9]{1,3}(?:.[0-9]{1,3})?(?:.[0-9]{1,3})?(.*?)$/, '');
6877
}
@@ -151,25 +160,105 @@ export class Package extends AsyncOptionalCreatable {
151160
fs.writeJsonSync(pkgJsonPath, this.packageJson);
152161
}
153162

163+
public getDistTags(name: string): Record<string, string> {
164+
const result = exec(`npm view ${name} dist-tags ${this.registry.getRegistryParameter()} --json`, {
165+
silent: true,
166+
});
167+
return JSON.parse(result.stdout) as Record<string, string>;
168+
}
169+
154170
public bumpResolutions(tag: string): void {
155171
if (!this.packageJson.resolutions) {
156172
throw new SfdxError('Bumping resolutions requires property "resolutions" to be present in package.json');
157173
}
158174

159175
Object.keys(this.packageJson.resolutions).map((key: string) => {
160-
const result = exec(`npm view ${key} dist-tags ${this.registry.getRegistryParameter()} --json`, {
161-
silent: true,
162-
});
163-
const versions = JSON.parse(result.stdout) as Record<string, string>;
176+
const versions = this.getDistTags(key);
164177
this.packageJson.resolutions[key] = versions[tag];
165178
});
166179
}
167180

181+
// Lookup dependency info by package name or npm alias
182+
// Examples: @salesforce/plugin-info or @sf/info
183+
// Pass in the dependencies you want to search through (dependencies, devDependencies, resolutions, etc)
184+
public getDependencyInfo(name: string, dependencies: Record<string, string>): DependencyInfo {
185+
for (const [key, value] of Object.entries(dependencies)) {
186+
if (key === name) {
187+
if (value.startsWith('npm:')) {
188+
// npm alias was passed in as name, so we need to parse package name and version
189+
// e.g. passed in: "@sf/login"
190+
// dependency: "@sf/login": "npm:@salesforce/plugin-login@1.1.1"
191+
return {
192+
dependencyName: key,
193+
packageName: parseAliasedPackageName(value),
194+
alias: value,
195+
currentVersion: parsePackageVersion(value),
196+
};
197+
} else {
198+
// package name was passed, so we can use key and value directly
199+
return {
200+
dependencyName: key,
201+
packageName: key,
202+
alias: null,
203+
currentVersion: value,
204+
};
205+
}
206+
}
207+
if (value.startsWith(`npm:${name}`)) {
208+
// package name was passed in as name, but an alias is used for the dependency
209+
// e.g. passed in: "@salesforce/plugin-login"
210+
// dependency: "@sf/login": "npm:@salesforce/plugin-login@1.1.1"
211+
return {
212+
dependencyName: key,
213+
packageName: name,
214+
alias: value,
215+
currentVersion: parsePackageVersion(value),
216+
};
217+
}
218+
}
219+
220+
cli.error(`${name} was not found in the dependencies section of the package.json`);
221+
}
222+
223+
public bumpDependencyVersions(targetDependencies: string[]): DependencyInfo[] {
224+
return targetDependencies
225+
.map((dep) => {
226+
// regex for npm package with optional namespace and version
227+
// https://regex101.com/r/HmIu3N/1
228+
const npmPackageRegex = /^((?:@[^/]+\/)?[^@/]+)(?:@([^@/]+))?$/;
229+
const [, name, version] = npmPackageRegex.exec(dep);
230+
231+
// We will look for packages in dependencies and resolutions
232+
const { dependencies, resolutions } = this.packageJson;
233+
234+
// find dependency in package.json (could be an npm alias)
235+
const depInfo = this.getDependencyInfo(name, { ...dependencies, ...resolutions });
236+
237+
// if a version is not provided, we'll look up the "latest" version
238+
depInfo.finalVersion = version ?? this.getDistTags(depInfo.packageName).latest;
239+
240+
// return if version did not change
241+
if (depInfo.currentVersion === depInfo.finalVersion) return;
242+
243+
// override final version if npm alias is used
244+
if (depInfo.alias) {
245+
depInfo.finalVersion = `npm:${depInfo.packageName}@${depInfo.finalVersion}`;
246+
}
247+
248+
// update dependency (or resolution) in package.json
249+
if (dependencies[depInfo.dependencyName]) {
250+
this.packageJson.dependencies[depInfo.dependencyName] = depInfo.finalVersion;
251+
} else {
252+
this.packageJson.resolutions[depInfo.dependencyName] = depInfo.finalVersion;
253+
}
254+
255+
return depInfo;
256+
})
257+
.filter(Boolean); // remove falsy values, in this case the `undefined` if version did not change
258+
}
259+
168260
public getNextRCVersion(tag: string, isPatch = false): string {
169-
const result = exec(`npm view ${this.packageJson.name} dist-tags ${this.registry.getRegistryParameter()} --json`, {
170-
silent: true,
171-
});
172-
const versions = JSON.parse(result.stdout) as Record<string, string>;
261+
const versions = this.getDistTags(this.packageJson.name);
173262

174263
const version = semver.parse(versions[tag]);
175264
return isPatch
@@ -217,10 +306,7 @@ export class Package extends AsyncOptionalCreatable {
217306
const pinnedPackages: PinnedPackage[] = [];
218307
deps.forEach((dep) => {
219308
// get the 'release' tag version or the version specified by the passed in tag
220-
const result = exec(`npm view ${dep.name} dist-tags ${this.registry.getRegistryParameter()} --json`, {
221-
silent: true,
222-
});
223-
const versions = JSON.parse(result.stdout) as Record<string, string>;
309+
const versions = this.getDistTags(dep.name);
224310
let tag = dep.tag;
225311

226312
// if tag is 'latest-rc' and there's no latest-rc release for a package, default to latest
@@ -244,9 +330,9 @@ export class Package extends AsyncOptionalCreatable {
244330

245331
// insert the new hardcoded versions into the dependencies in the project's package.json
246332
if (dep.alias) {
247-
this.packageJson['dependencies'][dep.alias] = `npm:${dep.name}@${version}`;
333+
this.packageJson.dependencies[dep.alias] = `npm:${dep.name}@${version}`;
248334
} else {
249-
this.packageJson['dependencies'][dep.name] = version;
335+
this.packageJson.dependencies[dep.name] = version;
250336
}
251337
// accumulate information to return
252338
pinnedPackages.push({ name: dep.name, version, tag, alias: dep.alias });

0 commit comments

Comments
 (0)