Skip to content

Commit

Permalink
feat: add dependencies updating controller
Browse files Browse the repository at this point in the history
  • Loading branch information
antongolub committed Nov 21, 2020
1 parent b3ff167 commit 0c9b040
Show file tree
Hide file tree
Showing 11 changed files with 411 additions and 172 deletions.
15 changes: 13 additions & 2 deletions bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ const cli = meow(
$ multi-semantic-release
Options
--sequential-init Avoid hypothetical concurrent initialization collisions.
--debug Output debugging information.
--sequential-init Avoid hypothetical concurrent initialization collisions.
--first-parent Apply commit filtering to current branch only.
--deps.bump Define deps version updating rule. Allowed: override, satisfy, inherit.
--deps.release Define release type for dependant package if any of its deps changes. Supported values: patch, minor, major, inherit.
--help Help info.
Examples
$ multi-semantic-release
$ multi-semantic-release --debug
$ multi-semantic-release --deps.bump=satisfy --deps.release=patch
`,
{
flags: {
Expand All @@ -28,6 +31,14 @@ const cli = meow(
debug: {
type: "boolean",
},
"deps.bump": {
type: "string",
default: "override",
},
"deps.release": {
type: "string",
default: "patch",
},
},
}
);
Expand Down
49 changes: 4 additions & 45 deletions lib/createInlinePluginCreator.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
const { writeFileSync } = require("fs");
const debug = require("debug")("msr:inlinePlugin");
const getCommitsFiltered = require("./getCommitsFiltered");
const getManifest = require("./getManifest");
const hasChangedDeep = require("./hasChangedDeep");
const recognizeFormat = require("./recognizeFormat");
const { get } = require("lodash");
const { updateManifestDeps, resolveReleaseType } = require("./updateDeps");

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

/**
* Update pkg deps.
* @param {Package} pkg The package this function is being called on.
* @param {string} path Path to package.json file
* @returns {undefined}
* @internal
*/
const updateManifestDeps = (pkg, path) => {
// Get and parse manifest file contents.
const manifest = getManifest(path);
const { indent, trailingWhitespace } = recognizeFormat(manifest.__contents__);
const updateDependency = (scope, name, version) => {
if (get(manifest, `${scope}.${name}`)) {
manifest[scope][name] = version;
}
};

// Loop through localDeps to update dependencies/devDependencies/peerDependencies in manifest.
pkg._localDeps.forEach((d) => {
// Get version of dependency.
const release = d._nextRelease || d._lastRelease;

// Cannot establish version.
if (!release || !release.version)
throw Error(`Cannot release because dependency ${d.name} has not been released`);

// Update version of dependency in manifest.
updateDependency("dependencies", d.name, release.version);
updateDependency("devDependencies", d.name, release.version);
updateDependency("peerDependencies", d.name, release.version);
updateDependency("optionalDependencies", d.name, release.version);
});

// Write package.json back out.
writeFileSync(path, JSON.stringify(manifest, null, indent) + trailingWhitespace);
};

/**
* Create an inline plugin for an individual package in a multirelease.
* This is called once per package and returns the inline plugin used for semanticRelease()
Expand All @@ -71,7 +30,7 @@ function createInlinePluginCreator(packages, multiContext, synchronizer, flags)
*/
function createInlinePlugin(pkg) {
// Vars.
const { deps, plugins, dir, path, name } = pkg;
const { deps, plugins, dir, name } = pkg;

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

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

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

// Update pkg deps.
updateManifestDeps(pkg, path);
updateManifestDeps(pkg);
pkg._depsUpdated = true;

// Vars.
Expand Down
25 changes: 0 additions & 25 deletions lib/hasChangedDeep.js

This file was deleted.

3 changes: 1 addition & 2 deletions lib/multiSemanticRelease.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
const { dirname } = require("path");
const semanticRelease = require("semantic-release");

const { check } = require("./blork");
const getLogger = require("./getLogger");
const getSynchronizer = require("./getSynchronizer");
Expand Down Expand Up @@ -48,7 +47,7 @@ async function multiSemanticRelease(
paths,
inputOptions = {},
{ cwd = process.cwd(), env = process.env, stdout = process.stdout, stderr = process.stderr } = {},
flags = {}
flags = { deps: {} }
) {
// Check params.
check(paths, "paths: string[]");
Expand Down
162 changes: 162 additions & 0 deletions lib/updateDeps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
const { writeFileSync } = require("fs");
const recognizeFormat = require("./recognizeFormat");
const semver = require("semver");

/**
* Resolve next package version.
*
* @param {Package} pkg Package object.
* @returns {string|undefined} Next pkg version.
* @internal
*/
const getNextVersion = (pkg) => {
const lastVersion = pkg._lastRelease && pkg._lastRelease.version;

return lastVersion && typeof pkg._nextType === "string" ? semver.inc(lastVersion, pkg._nextType) : "1.0.0";
};

/**
* Resolve package release type taking into account the cascading dependency update.
*
* @param {Package} pkg Package object.
* @param {string|undefined} bumpStrategy Dependency resolution strategy: override, satisfy, inherit.
* @param {string|undefined} releaseStrategy Release type triggered by deps updating: patch, minor, major, inherit.
* @param {Package[]} ignore=[] Packages to ignore (to prevent infinite loops).
* @returns {string|undefined} Resolved release type.
* @internal
*/
const resolveReleaseType = (pkg, bumpStrategy = "override", releaseStrategy = "patch", ignore = []) => {
// NOTE This fn also updates pkg deps, so it must be invoked anyway.
const dependantReleaseType = getDependantRelease(pkg, bumpStrategy, releaseStrategy, ignore);

// Release type found by commitAnalyzer.
if (pkg._nextType) {
return pkg._nextType;
}

if (!dependantReleaseType) {
return undefined;
}

pkg._nextType = releaseStrategy === "inherit" ? dependantReleaseType : releaseStrategy;

return pkg._nextType;
};

/**
* Get dependant release type by recursive scanning and updating its deps.
*
* @param {Package} pkg The package with local deps to check.
* @param {string} bumpStrategy Dependency resolution strategy: override, satisfy, inherit.
* @param {string} releaseStrategy Release type triggered by deps updating: patch, minor, major, inherit.
* @param {Package[]} ignore Packages to ignore (to prevent infinite loops).
* @returns {string|undefined} Returns the highest release type if found, undefined otherwise
* @internal
*/
const getDependantRelease = (pkg, bumpStrategy, releaseStrategy, ignore) => {
const severityOrder = ["patch", "minor", "major"];
const { _localDeps, manifest = {} } = pkg;
const { dependencies = {}, devDependencies = {}, peerDependencies = {}, optionalDependencies = {} } = manifest;
const scopes = [dependencies, devDependencies, peerDependencies, optionalDependencies];
const bumpDependency = (scope, name, nextVersion) => {
const currentVersion = scope[name];
if (!nextVersion || !currentVersion) {
return;
}
const resolvedVersion = resolveNextVersion(currentVersion, nextVersion, releaseStrategy);

if (currentVersion !== resolvedVersion) {
scope[name] = resolvedVersion;

return true;
}
};

// prettier-ignore
return _localDeps
.filter((p) => ignore.indexOf(p) === -1)
.reduce((releaseType, p) => {
const name = p.name;

// Has changed if...
// 1. Any local dep package itself has changed
// 2. Any local dep package has local deps that have changed.
const nextType = resolveReleaseType(p, bumpStrategy, releaseStrategy,[...ignore, ..._localDeps]);
const nextVersion = getNextVersion(p);
const lastVersion = pkg._lastRelease && pkg._lastRelease.version;

// 3. And this change should correspond to manifest updating rule.
const requireRelease = [
...scopes.map((scope) => bumpDependency(scope, name, nextVersion)),
].some(v => v) || !lastVersion;

return requireRelease && (severityOrder.indexOf(nextType) > severityOrder.indexOf(releaseType))
? nextType
: releaseType;
}, undefined);
};

/**
* Resolve next version of dependency.
*
* @param {string} currentVersion Current dep version
* @param {string} nextVersion Next release type: patch, minor, major
* @param {string|undefined} strategy Resolution strategy: inherit, override, satisfy
* @returns {string} Next dependency version
* @internal
*/
const resolveNextVersion = (currentVersion, nextVersion, strategy = "override") => {
if (strategy === "satisfy" && semver.satisfies(nextVersion, currentVersion)) {
return currentVersion;
}

if (strategy === "inherit") {
const sep = ".";
const nextChunks = nextVersion.split(sep);
const currentChunks = currentVersion.split(sep);
// prettier-ignore
const resolvedChunks = currentChunks.map((chunk, i) =>
nextChunks[i]
? chunk.replace(/\d+/, nextChunks[i])
: chunk
);

return resolvedChunks.join(sep);
}

// By default next package version would be set as is for the all dependants
return nextVersion;
};

/**
* Update pkg deps.
*
* @param {Package} pkg The package this function is being called on.
* @param {string} strategy Dependency version updating rule
* @returns {undefined}
* @internal
*/
const updateManifestDeps = (pkg, strategy) => {
const { manifest, path } = pkg;
const { indent, trailingWhitespace } = recognizeFormat(manifest.__contents__);

// Loop through localDeps to verify release consistency.
pkg._localDeps.forEach((d) => {
// Get version of dependency.
const release = d._nextRelease || d._lastRelease;

// Cannot establish version.
if (!release || !release.version)
throw Error(`Cannot release because dependency ${d.name} has not been released`);
});

// Write package.json back out.
writeFileSync(path, JSON.stringify(manifest, null, indent) + trailingWhitespace);
};

module.exports = {
getNextVersion,
updateManifestDeps,
resolveReleaseType,
resolveNextVersion,
};
2 changes: 1 addition & 1 deletion test/fixtures/yarnWorkspaces/packages/d/package.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"name": "msr-test-d",
"version": "0.0.0"
}
}

0 comments on commit 0c9b040

Please sign in to comment.