Skip to content

Commit 0c9b040

Browse files
committed
feat: add dependencies updating controller
1 parent b3ff167 commit 0c9b040

11 files changed

+411
-172
lines changed

bin/cli.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@ const cli = meow(
99
$ multi-semantic-release
1010
1111
Options
12-
--sequential-init Avoid hypothetical concurrent initialization collisions.
1312
--debug Output debugging information.
13+
--sequential-init Avoid hypothetical concurrent initialization collisions.
1414
--first-parent Apply commit filtering to current branch only.
15+
--deps.bump Define deps version updating rule. Allowed: override, satisfy, inherit.
16+
--deps.release Define release type for dependant package if any of its deps changes. Supported values: patch, minor, major, inherit.
1517
--help Help info.
1618
1719
Examples
18-
$ multi-semantic-release
20+
$ multi-semantic-release --debug
21+
$ multi-semantic-release --deps.bump=satisfy --deps.release=patch
1922
`,
2023
{
2124
flags: {
@@ -28,6 +31,14 @@ const cli = meow(
2831
debug: {
2932
type: "boolean",
3033
},
34+
"deps.bump": {
35+
type: "string",
36+
default: "override",
37+
},
38+
"deps.release": {
39+
type: "string",
40+
default: "patch",
41+
},
3142
},
3243
}
3344
);

lib/createInlinePluginCreator.js

Lines changed: 4 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
const { writeFileSync } = require("fs");
21
const debug = require("debug")("msr:inlinePlugin");
32
const getCommitsFiltered = require("./getCommitsFiltered");
4-
const getManifest = require("./getManifest");
5-
const hasChangedDeep = require("./hasChangedDeep");
6-
const recognizeFormat = require("./recognizeFormat");
7-
const { get } = require("lodash");
3+
const { updateManifestDeps, resolveReleaseType } = require("./updateDeps");
84

95
/**
106
* Create an inline plugin creator for a multirelease.
@@ -23,43 +19,6 @@ function createInlinePluginCreator(packages, multiContext, synchronizer, flags)
2319
const { cwd } = multiContext;
2420
const { todo, waitFor, waitForAll, emit, getLucky } = synchronizer;
2521

26-
/**
27-
* Update pkg deps.
28-
* @param {Package} pkg The package this function is being called on.
29-
* @param {string} path Path to package.json file
30-
* @returns {undefined}
31-
* @internal
32-
*/
33-
const updateManifestDeps = (pkg, path) => {
34-
// Get and parse manifest file contents.
35-
const manifest = getManifest(path);
36-
const { indent, trailingWhitespace } = recognizeFormat(manifest.__contents__);
37-
const updateDependency = (scope, name, version) => {
38-
if (get(manifest, `${scope}.${name}`)) {
39-
manifest[scope][name] = version;
40-
}
41-
};
42-
43-
// Loop through localDeps to update dependencies/devDependencies/peerDependencies in manifest.
44-
pkg._localDeps.forEach((d) => {
45-
// Get version of dependency.
46-
const release = d._nextRelease || d._lastRelease;
47-
48-
// Cannot establish version.
49-
if (!release || !release.version)
50-
throw Error(`Cannot release because dependency ${d.name} has not been released`);
51-
52-
// Update version of dependency in manifest.
53-
updateDependency("dependencies", d.name, release.version);
54-
updateDependency("devDependencies", d.name, release.version);
55-
updateDependency("peerDependencies", d.name, release.version);
56-
updateDependency("optionalDependencies", d.name, release.version);
57-
});
58-
59-
// Write package.json back out.
60-
writeFileSync(path, JSON.stringify(manifest, null, indent) + trailingWhitespace);
61-
};
62-
6322
/**
6423
* Create an inline plugin for an individual package in a multirelease.
6524
* This is called once per package and returns the inline plugin used for semanticRelease()
@@ -71,7 +30,7 @@ function createInlinePluginCreator(packages, multiContext, synchronizer, flags)
7130
*/
7231
function createInlinePlugin(pkg) {
7332
// Vars.
74-
const { deps, plugins, dir, path, name } = pkg;
33+
const { deps, plugins, dir, name } = pkg;
7534

7635
/**
7736
* @var {Commit[]} List of _filtered_ commits that only apply to this package.
@@ -141,7 +100,7 @@ function createInlinePluginCreator(packages, multiContext, synchronizer, flags)
141100
await waitForAll("_analyzed");
142101

143102
// Make sure type is "patch" if the package has any deps that have changed.
144-
if (!pkg._nextType && hasChangedDeep(pkg._localDeps)) pkg._nextType = "patch";
103+
pkg._nextType = resolveReleaseType(pkg, flags.deps.bump, flags.deps.release);
145104

146105
debug("commits analyzed: %s", pkg.name);
147106
debug("release type: %s", pkg._nextType);
@@ -188,7 +147,7 @@ function createInlinePluginCreator(packages, multiContext, synchronizer, flags)
188147
await waitFor("_readyToGenerateNotes", pkg);
189148

190149
// Update pkg deps.
191-
updateManifestDeps(pkg, path);
150+
updateManifestDeps(pkg);
192151
pkg._depsUpdated = true;
193152

194153
// Vars.

lib/hasChangedDeep.js

Lines changed: 0 additions & 25 deletions
This file was deleted.

lib/multiSemanticRelease.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
const { dirname } = require("path");
22
const semanticRelease = require("semantic-release");
3-
43
const { check } = require("./blork");
54
const getLogger = require("./getLogger");
65
const getSynchronizer = require("./getSynchronizer");
@@ -48,7 +47,7 @@ async function multiSemanticRelease(
4847
paths,
4948
inputOptions = {},
5049
{ cwd = process.cwd(), env = process.env, stdout = process.stdout, stderr = process.stderr } = {},
51-
flags = {}
50+
flags = { deps: {} }
5251
) {
5352
// Check params.
5453
check(paths, "paths: string[]");

lib/updateDeps.js

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
const { writeFileSync } = require("fs");
2+
const recognizeFormat = require("./recognizeFormat");
3+
const semver = require("semver");
4+
5+
/**
6+
* Resolve next package version.
7+
*
8+
* @param {Package} pkg Package object.
9+
* @returns {string|undefined} Next pkg version.
10+
* @internal
11+
*/
12+
const getNextVersion = (pkg) => {
13+
const lastVersion = pkg._lastRelease && pkg._lastRelease.version;
14+
15+
return lastVersion && typeof pkg._nextType === "string" ? semver.inc(lastVersion, pkg._nextType) : "1.0.0";
16+
};
17+
18+
/**
19+
* Resolve package release type taking into account the cascading dependency update.
20+
*
21+
* @param {Package} pkg Package object.
22+
* @param {string|undefined} bumpStrategy Dependency resolution strategy: override, satisfy, inherit.
23+
* @param {string|undefined} releaseStrategy Release type triggered by deps updating: patch, minor, major, inherit.
24+
* @param {Package[]} ignore=[] Packages to ignore (to prevent infinite loops).
25+
* @returns {string|undefined} Resolved release type.
26+
* @internal
27+
*/
28+
const resolveReleaseType = (pkg, bumpStrategy = "override", releaseStrategy = "patch", ignore = []) => {
29+
// NOTE This fn also updates pkg deps, so it must be invoked anyway.
30+
const dependantReleaseType = getDependantRelease(pkg, bumpStrategy, releaseStrategy, ignore);
31+
32+
// Release type found by commitAnalyzer.
33+
if (pkg._nextType) {
34+
return pkg._nextType;
35+
}
36+
37+
if (!dependantReleaseType) {
38+
return undefined;
39+
}
40+
41+
pkg._nextType = releaseStrategy === "inherit" ? dependantReleaseType : releaseStrategy;
42+
43+
return pkg._nextType;
44+
};
45+
46+
/**
47+
* Get dependant release type by recursive scanning and updating its deps.
48+
*
49+
* @param {Package} pkg The package with local deps to check.
50+
* @param {string} bumpStrategy Dependency resolution strategy: override, satisfy, inherit.
51+
* @param {string} releaseStrategy Release type triggered by deps updating: patch, minor, major, inherit.
52+
* @param {Package[]} ignore Packages to ignore (to prevent infinite loops).
53+
* @returns {string|undefined} Returns the highest release type if found, undefined otherwise
54+
* @internal
55+
*/
56+
const getDependantRelease = (pkg, bumpStrategy, releaseStrategy, ignore) => {
57+
const severityOrder = ["patch", "minor", "major"];
58+
const { _localDeps, manifest = {} } = pkg;
59+
const { dependencies = {}, devDependencies = {}, peerDependencies = {}, optionalDependencies = {} } = manifest;
60+
const scopes = [dependencies, devDependencies, peerDependencies, optionalDependencies];
61+
const bumpDependency = (scope, name, nextVersion) => {
62+
const currentVersion = scope[name];
63+
if (!nextVersion || !currentVersion) {
64+
return;
65+
}
66+
const resolvedVersion = resolveNextVersion(currentVersion, nextVersion, releaseStrategy);
67+
68+
if (currentVersion !== resolvedVersion) {
69+
scope[name] = resolvedVersion;
70+
71+
return true;
72+
}
73+
};
74+
75+
// prettier-ignore
76+
return _localDeps
77+
.filter((p) => ignore.indexOf(p) === -1)
78+
.reduce((releaseType, p) => {
79+
const name = p.name;
80+
81+
// Has changed if...
82+
// 1. Any local dep package itself has changed
83+
// 2. Any local dep package has local deps that have changed.
84+
const nextType = resolveReleaseType(p, bumpStrategy, releaseStrategy,[...ignore, ..._localDeps]);
85+
const nextVersion = getNextVersion(p);
86+
const lastVersion = pkg._lastRelease && pkg._lastRelease.version;
87+
88+
// 3. And this change should correspond to manifest updating rule.
89+
const requireRelease = [
90+
...scopes.map((scope) => bumpDependency(scope, name, nextVersion)),
91+
].some(v => v) || !lastVersion;
92+
93+
return requireRelease && (severityOrder.indexOf(nextType) > severityOrder.indexOf(releaseType))
94+
? nextType
95+
: releaseType;
96+
}, undefined);
97+
};
98+
99+
/**
100+
* Resolve next version of dependency.
101+
*
102+
* @param {string} currentVersion Current dep version
103+
* @param {string} nextVersion Next release type: patch, minor, major
104+
* @param {string|undefined} strategy Resolution strategy: inherit, override, satisfy
105+
* @returns {string} Next dependency version
106+
* @internal
107+
*/
108+
const resolveNextVersion = (currentVersion, nextVersion, strategy = "override") => {
109+
if (strategy === "satisfy" && semver.satisfies(nextVersion, currentVersion)) {
110+
return currentVersion;
111+
}
112+
113+
if (strategy === "inherit") {
114+
const sep = ".";
115+
const nextChunks = nextVersion.split(sep);
116+
const currentChunks = currentVersion.split(sep);
117+
// prettier-ignore
118+
const resolvedChunks = currentChunks.map((chunk, i) =>
119+
nextChunks[i]
120+
? chunk.replace(/\d+/, nextChunks[i])
121+
: chunk
122+
);
123+
124+
return resolvedChunks.join(sep);
125+
}
126+
127+
// By default next package version would be set as is for the all dependants
128+
return nextVersion;
129+
};
130+
131+
/**
132+
* Update pkg deps.
133+
*
134+
* @param {Package} pkg The package this function is being called on.
135+
* @param {string} strategy Dependency version updating rule
136+
* @returns {undefined}
137+
* @internal
138+
*/
139+
const updateManifestDeps = (pkg, strategy) => {
140+
const { manifest, path } = pkg;
141+
const { indent, trailingWhitespace } = recognizeFormat(manifest.__contents__);
142+
143+
// Loop through localDeps to verify release consistency.
144+
pkg._localDeps.forEach((d) => {
145+
// Get version of dependency.
146+
const release = d._nextRelease || d._lastRelease;
147+
148+
// Cannot establish version.
149+
if (!release || !release.version)
150+
throw Error(`Cannot release because dependency ${d.name} has not been released`);
151+
});
152+
153+
// Write package.json back out.
154+
writeFileSync(path, JSON.stringify(manifest, null, indent) + trailingWhitespace);
155+
};
156+
157+
module.exports = {
158+
getNextVersion,
159+
updateManifestDeps,
160+
resolveReleaseType,
161+
resolveNextVersion,
162+
};
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
22
"name": "msr-test-d",
33
"version": "0.0.0"
4-
}
4+
}

0 commit comments

Comments
 (0)