diff --git a/lib/createInlinePluginCreator.js b/lib/createInlinePluginCreator.js index 45ab1b24..e9ec0600 100644 --- a/lib/createInlinePluginCreator.js +++ b/lib/createInlinePluginCreator.js @@ -107,8 +107,27 @@ function createInlinePluginCreator(packages, multiContext, synchronizer, flags) pkg._analyzed = true; await waitForAll("_analyzed"); - // Make sure type is "patch" if the package has any deps that have changed. - pkg._nextType = resolveReleaseType(pkg, flags.deps.bump, flags.deps.release); + //go in rounds to check for dependency changes that impact the release type until no changes + //are found in any of the packages. Doing this in rounds will have the changes "bubble-up" in + //the dependency graph until all have been processed. + + let round = 0; + let stable = false; + + while (!stable) { + const signal = "_depCheck" + round++; + + //estimate the type of update for the package + const nextType = resolveReleaseType(pkg, flags.deps.bump, flags.deps.release); + + //indicate if it changed + pkg[signal] = pkg._nextType === nextType ? "stable" : "changed"; + pkg._nextType = nextType; + + await waitForAll(signal); + + stable = packages.every((p) => p[signal] === "stable"); + } debug("commits analyzed: %s", pkg.name); debug("release type: %s", pkg._nextType); diff --git a/lib/updateDeps.js b/lib/updateDeps.js index 0f4116ef..7cd6cc24 100644 --- a/lib/updateDeps.js +++ b/lib/updateDeps.js @@ -124,58 +124,67 @@ const _nextPreHighestVersion = (latestTag, lastVersion, pkgPreRelease) => { return bumpFromTags ? getHighestVersion(bumpFromLast, bumpFromTags) : bumpFromLast; }; +/** + * Returns the 'highest' type of release update, major > minor > patch > undefined. + * @param {...string} releaseTypes types (patch | minor | major | undefined) of which the highest to return. + * @returns {string} release type considered highest + */ +const getHighestReleaseType = (...releaseTypes) => + ["major", "minor", "patch"].find((type) => releaseTypes.includes(type)); + /** * 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 when traversing graphs of dependencies). * @returns {string|undefined} Resolved release type. * @internal */ -const resolveReleaseType = (pkg, bumpStrategy = "override", releaseStrategy = "patch", ignore = []) => { - //make sure any dependency changes are resolved before returning the release type - if (!pkg._depsResolved) { - //create a list of dependencies that require change - pkg._depsChanged = pkg.localDeps - .filter((d) => !ignore.includes(d)) - .filter((d) => needsDependencyUpdate(pkg, d, bumpStrategy, releaseStrategy, [pkg, ...ignore])); - - //get the (preliminary) release type of the package based on release strategy (and analyzed changed dependencies) - pkg._nextType = getDependentRelease(pkg, releaseStrategy); - - //indicate that all deps are resolved (fixates the next type and depsChanged) - pkg._depsResolved = ignore.length === 0; +const resolveReleaseType = (pkg, bumpStrategy = "override", releaseStrategy = "patch") => { + // Define release type for dependent package if any of its deps changes. + // `patch`, `minor`, `major` — strictly declare the release type that occurs when any dependency is updated. + // `inherit` — applies the "highest" release of updated deps to the package. + // For example, if any dep has a breaking change, `major` release will be applied to the all dependants up the chain. + + //create a list of dependencies that require change to the manifest + pkg._depsChanged = pkg.localDeps.filter((d) => needsDependencyUpdate(pkg, d, bumpStrategy)); + + //check if any dependencies have changed. If not return the current type of release + if ( + !pkg._lastRelease || //not released yet + pkg._depsChanged.length === 0 || //no deps available + pkg._depsChanged.every((dep) => dep._lastRelease && !dep._nextType) //no new deps or deps upgraded + ) + return pkg._nextType; + + //find highest release type if strategy is inherit, starting of type set by commit analyzer + if (releaseStrategy === "inherit") { + return getHighestReleaseType(pkg._nextType, ...pkg._depsChanged.map((d) => d._nextType)); } - return pkg._nextType; + //set to highest of commit analyzer found change and releaseStrategy + //releaseStrategy of major could override local update of minor + return getHighestReleaseType(pkg._nextType, releaseStrategy); }; /** * Indicates if the manifest file requires a change for the given dependency * @param {Package} pkg Package object. + * @param {Package} dependency dependency to check * @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 {boolean } true if dependency needs to change */ - -const needsDependencyUpdate = (pkg, dependency, bumpStrategy, releaseStrategy, ignore) => { +const needsDependencyUpdate = (pkg, dependency, bumpStrategy) => { //get last release of dependency const depLastVersion = dependency._lastRelease && dependency._lastRelease.version; - // 3. check if dependency was released before (if not, this is assumed to be a new package + dependency) + //Check if dependency was released before (if not, this is assumed to be a new package + dependency) const wasReleased = depLastVersion !== undefined; if (!wasReleased) return true; //new packages always require a package re-release - //get nextType of the dependency (recursion occurs here!) - // Has changed if... - // 1. Any local dep package itself triggered changed - // 2. Any local dep package has local deps that triggered change. - const depNextType = resolveReleaseType(dependency, bumpStrategy, releaseStrategy, ignore); - //get estimated next version of dependency (which is lastVersion if no change expected) - const depNextVersion = depNextType + const depNextVersion = dependency._nextType ? dependency._preRelease ? getNextPreVersion(dependency) : getNextVersion(dependency) @@ -185,7 +194,7 @@ const needsDependencyUpdate = (pkg, dependency, bumpStrategy, releaseStrategy, i const { dependencies = {}, devDependencies = {}, peerDependencies = {}, optionalDependencies = {} } = pkg.manifest; const scopes = [dependencies, devDependencies, peerDependencies, optionalDependencies]; - // 4. Check if the manifest dependency rules warrants an update (in any of the dependency scopes) + //Check if the manifest dependency rules warrants an update (in any of the dependency scopes) const requireUpdate = scopes.some((scope) => manifestUpdateNecessary(scope, dependency.name, depNextVersion, bumpStrategy) ); @@ -227,6 +236,9 @@ const manifestUpdateNecessary = (scope, name, nextVersion, bumpStrategy) => { * @internal */ const resolveNextVersion = (currentVersion, nextVersion, bumpStrategy = "override") => { + //no change... + if (currentVersion === nextVersion) return currentVersion; + // Check the next pkg version against its current references. // If it matches (`*` matches to any, `1.1.0` matches `1.1.x`, `1.5.0` matches to `^1.0.0` and so on) // release will not be triggered, if not `override` strategy will be applied instead. @@ -256,56 +268,16 @@ const resolveNextVersion = (currentVersion, nextVersion, bumpStrategy = "overrid return nextVersion; }; -/** - * Get dependent release type by analyzing the current nextType and changed dependencies - * @param {Package} pkg The package to determine next type of release of - * @param {string} releaseStrategy Release type triggered by deps updating: patch, minor, major, inherit. - * @returns {string|undefined} Returns the highest release type if found, undefined otherwise - * @internal - */ -const getDependentRelease = (pkg, releaseStrategy) => { - const severityOrder = ["patch", "minor", "major"]; - - // Define release type for dependent package if any of its deps changes. - // `patch`, `minor`, `major` — strictly declare the release type that occurs when any dependency is updated. - // `inherit` — applies the "highest" release of updated deps to the package. - // For example, if any dep has a breaking change, `major` release will be applied to the all dependants up the chain. - - //return type set by commit analyzer if no deps changed - if ( - !pkg._lastRelease || //new package - !pkg._depsChanged || //no deps analyzed - pkg._depsChanged.length === 0 || //no deps available - pkg._depsChanged.every((dep) => !dep._nextType && dep._lastRelease) //no new deps or deps upgraded - ) - return pkg._nextType; - - if (releaseStrategy === "inherit") { - //find highest release type if strategy is inherit, starting of type set by commit analyzer - return pkg._depsChanged.reduce((maxReleaseType, dependency) => { - return severityOrder.indexOf(dependency._nextType) > severityOrder.indexOf(maxReleaseType) - ? dependency._nextType - : maxReleaseType; - }, pkg._nextType); - } - - //return highest of commit analyzer found change and releaseStrategy - //releaseStrategy of major could override local update of minor - return severityOrder.indexOf(pkg._nextType) > severityOrder.indexOf(releaseStrategy) - ? pkg._nextType - : releaseStrategy; -}; - /** * Update pkg deps. * * @param {Package} pkg The package this function is being called on. + * @param {boolean} writeOut Commit the package to the file store (set to false to suppres) * @returns {undefined} * @internal */ -const updateManifestDeps = (pkg) => { +const updateManifestDeps = (pkg, writeOut = true) => { const { manifest, path } = pkg; - const { indent, trailingWhitespace } = recognizeFormat(manifest.__contents__); // Loop through changed deps to verify release consistency. pkg._depsChanged.forEach((dependency) => { @@ -329,11 +301,12 @@ const updateManifestDeps = (pkg) => { }); }); - if (!auditManifestChanges(manifest, path)) { + if (!writeOut || !auditManifestChanges(manifest, path)) { return; } // Write package.json back out. + const { indent, trailingWhitespace } = recognizeFormat(manifest.__contents__); writeFileSync(path, JSON.stringify(manifest, null, indent) + trailingWhitespace); }; diff --git a/test/fixtures/yarnWorkspacesMutualDependency/package.json b/test/fixtures/yarnWorkspacesMutualDependency/package.json new file mode 100644 index 00000000..c33668ba --- /dev/null +++ b/test/fixtures/yarnWorkspacesMutualDependency/package.json @@ -0,0 +1,20 @@ +{ + "name": "msr-test-yarn", + "author": "Joost Reuzel", + "version": "0.0.0-semantically-released", + "private": true, + "license": "0BSD", + "engines": { + "node": ">=8.3" + }, + "workspaces": [ + "packages/*" + ], + "release": { + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator" + ], + "noCi": true + } +} diff --git a/test/fixtures/yarnWorkspacesMutualDependency/packages/a/package.json b/test/fixtures/yarnWorkspacesMutualDependency/packages/a/package.json new file mode 100644 index 00000000..bcaf9de5 --- /dev/null +++ b/test/fixtures/yarnWorkspacesMutualDependency/packages/a/package.json @@ -0,0 +1,7 @@ +{ + "name": "msr-test-a", + "version": "1.0.0", + "dependencies": { + "msr-test-b": "1.0.0" + } +} \ No newline at end of file diff --git a/test/fixtures/yarnWorkspacesMutualDependency/packages/b/package.json b/test/fixtures/yarnWorkspacesMutualDependency/packages/b/package.json new file mode 100644 index 00000000..9ffcf338 --- /dev/null +++ b/test/fixtures/yarnWorkspacesMutualDependency/packages/b/package.json @@ -0,0 +1,7 @@ +{ + "name": "msr-test-b", + "version": "1.0.0", + "dependencies": { + "msr-test-a": "1.0.0" + } +} diff --git a/test/lib/multiSemanticRelease.test.js b/test/lib/multiSemanticRelease.test.js index a08da33f..8927209d 100644 --- a/test/lib/multiSemanticRelease.test.js +++ b/test/lib/multiSemanticRelease.test.js @@ -1060,4 +1060,99 @@ describe("multiSemanticRelease()", () => { message: expect.stringMatching("Package peerDependencies must be object"), }); }); + + test("Changes in packages with mutual dependency", async () => { + // Create Git repo. + const cwd = gitInit(); + // Initial commit. + copyDirectory(`test/fixtures/yarnWorkspacesMutualDependency/`, cwd); + const sha1 = gitCommitAll(cwd, "feat: Initial release"); + gitTag(cwd, "msr-test-a@1.0.0"); + gitTag(cwd, "msr-test-b@1.0.0"); + // Second commit. + writeFileSync(`${cwd}/packages/a/aaa.txt`, "AAA"); + const sha2 = gitCommitAll(cwd, "feat(aaa): Add missing text file"); + const url = gitInitOrigin(cwd); + gitPush(cwd); + + // Capture output. + const stdout = new WritableStreamBuffer(); + const stderr = new WritableStreamBuffer(); + + // Call multiSemanticRelease() + // Doesn't include plugins that actually publish. + const multiSemanticRelease = require("../../"); + const result = await multiSemanticRelease( + [`packages/a/package.json`, `packages/b/package.json`], + {}, + { cwd, stdout, stderr }, + { deps: { bump: "satisfy" }, dryRun: false } + ); + + // Get stdout and stderr output. + const err = stderr.getContentsAsString("utf8"); + expect(err).toBe(false); + const out = stdout.getContentsAsString("utf8"); + expect(out).toMatch("Started multirelease! Loading 2 packages..."); + expect(out).toMatch("Loaded package msr-test-a"); + expect(out).toMatch("Loaded package msr-test-b"); + expect(out).toMatch("Queued 2 packages! Starting release..."); + expect(out).toMatch("Created tag msr-test-a@1.1.0"); + expect(out).toMatch("Created tag msr-test-b@1.0.1"); + expect(out).toMatch("Released 2 of 2 packages, semantically!"); + + const a = 0; + const b = 1; + // A. + expect(result[a].name).toBe("msr-test-a"); + expect(result[a].result.lastRelease).toMatchObject({ + gitHead: sha1, + gitTag: "msr-test-a@1.0.0", + version: "1.0.0", + }); + expect(result[a].result.nextRelease).toMatchObject({ + gitHead: sha2, + gitTag: "msr-test-a@1.1.0", + type: "minor", + version: "1.1.0", + }); + expect(result[a].result.nextRelease.notes).toMatch("# msr-test-a [1.1.0]"); + expect(result[a].result.nextRelease.notes).toMatch("### Features\n\n* **aaa:** Add missing text file"); + expect(result[a].result.nextRelease.notes).toMatch("### Dependencies\n\n* **msr-test-b:** upgraded to 1.0.1"); + + // B. + expect(result[b].name).toBe("msr-test-b"); + expect(result[b].result.lastRelease).toEqual({ + channels: [null], + gitHead: sha1, + gitTag: "msr-test-b@1.0.0", + name: "msr-test-b@1.0.0", + version: "1.0.0", + }); + expect(result[b].result.nextRelease).toMatchObject({ + gitHead: sha2, + gitTag: "msr-test-b@1.0.1", + type: "patch", + version: "1.0.1", + }); + expect(result[b].result.nextRelease.notes).toMatch("# msr-test-b [1.0.1]"); + expect(result[b].result.nextRelease.notes).not.toMatch("### Features"); + expect(result[b].result.nextRelease.notes).not.toMatch("### Bug Fixes"); + expect(result[b].result.nextRelease.notes).toMatch("### Dependencies\n\n* **msr-test-a:** upgraded to 1.1.0"); + + // ONLY 3 times. + expect(result[2]).toBe(undefined); + + // Check manifests. + expect(require(`${cwd}/packages/a/package.json`)).toMatchObject({ + dependencies: { + "msr-test-b": "1.0.1", + }, + }); + expect(require(`${cwd}/packages/b/package.json`)).toMatchObject({ + dependencies: { + "msr-test-a": "1.1.0", + }, + }); + }); }); diff --git a/test/lib/resolveReleaseType.test.js b/test/lib/resolveReleaseType.test.js index d362c21d..30e50f51 100644 --- a/test/lib/resolveReleaseType.test.js +++ b/test/lib/resolveReleaseType.test.js @@ -10,11 +10,13 @@ describe("resolveReleaseType()", () => { test("does patch with no dependencies", () => { const pkg1 = { _nextType: "patch", localDeps: [] }; expect(resolveReleaseType(pkg1)).toBe("patch"); + expect(pkg1._depsChanged.length).toBe(0); }); test("new package", () => { const pkg2 = { _nextType: undefined, localDeps: [] }; expect(resolveReleaseType(pkg2)).toBe(undefined); + expect(pkg2._depsChanged.length).toBe(0); }); test("new with dependencies", () => { @@ -26,6 +28,7 @@ describe("resolveReleaseType()", () => { ], }; expect(resolveReleaseType(pkg3)).toBe(undefined); + expect(pkg3._depsChanged.length).toBe(2); }); test("upgrades if dependency upgrades", () => { @@ -39,6 +42,8 @@ describe("resolveReleaseType()", () => { _lastRelease: { version: "1.0.0" }, }; expect(resolveReleaseType(pkg4)).toBe("patch"); + expect(pkg4._depsChanged.length).toBe(1); + expect(pkg4._depsChanged[0].name).toBe("a"); }); test("doesn't do updates if no updates", () => { @@ -52,6 +57,7 @@ describe("resolveReleaseType()", () => { _lastRelease: { version: "1.0.0" }, }; expect(resolveReleaseType(pkg5)).toBe(undefined); + expect(pkg5._depsChanged.length).toBe(0); }); test("inherits the highest update", () => { @@ -73,7 +79,130 @@ describe("resolveReleaseType()", () => { ], _lastRelease: { version: "1.0.0" }, }; - expect(resolveReleaseType(pkg6, "override", "inherit")).toBe("major"); + + //first round + pkg6._nextType = resolveReleaseType(pkg6, "override", "inherit"); + pkg6.localDeps[0]._nextType = resolveReleaseType(pkg6.localDeps[0], "override", "inherit"); + pkg6.localDeps[0].localDeps.forEach( + (dep) => (dep._nextType = resolveReleaseType(dep, "override", "inherit")) + ); + + expect(pkg6._nextType).toBe(undefined); + expect(pkg6._depsChanged.length).toBe(0); + expect(pkg6.localDeps[0]._nextType).toBe("major"); + expect(pkg6.localDeps[0]._depsChanged.length).toBe(2); + expect(pkg6.localDeps[0].localDeps[0]._nextType).toBe(false); + expect(pkg6.localDeps[0].localDeps[1]._nextType).toBe("patch"); + expect(pkg6.localDeps[0].localDeps[2]._nextType).toBe("major"); + + //second round + pkg6._nextType = resolveReleaseType(pkg6, "override", "inherit"); + pkg6.localDeps[0]._nextType = resolveReleaseType(pkg6.localDeps[0], "override", "inherit"); + pkg6.localDeps[0].localDeps.forEach( + (dep) => (dep._nextType = resolveReleaseType(dep, "override", "inherit")) + ); + + expect(pkg6._nextType).toBe("major"); + expect(pkg6._depsChanged.length).toBe(1); + expect(pkg6.localDeps[0]._nextType).toBe("major"); + expect(pkg6.localDeps[0]._depsChanged.length).toBe(2); + expect(pkg6.localDeps[0].localDeps[0]._nextType).toBe(false); + expect(pkg6.localDeps[0].localDeps[1]._nextType).toBe("patch"); + expect(pkg6.localDeps[0].localDeps[2]._nextType).toBe("major"); + }); + + test("overrides dependent release type with custom value if defined", () => { + const pkg6 = { + manifest: { dependencies: { a: "1.0.0" } }, + _nextType: undefined, + localDeps: [ + { + name: "a", + _lastRelease: { version: "1.0.0" }, + _nextType: false, + manifest: { dependencies: { b: "1.0.0", c: "1.0.0", d: "1.0.0" } }, + localDeps: [ + { name: "b", _nextType: false, localDeps: [], _lastRelease: { version: "1.0.0" } }, + { name: "c", _nextType: "patch", localDeps: [], _lastRelease: { version: "1.0.0" } }, + { name: "d", _nextType: "minor", localDeps: [], _lastRelease: { version: "1.0.0" } }, + ], + }, + ], + _lastRelease: { version: "1.0.0" }, + }; + + //first round + pkg6._nextType = resolveReleaseType(pkg6, undefined, "major"); + pkg6.localDeps[0]._nextType = resolveReleaseType(pkg6.localDeps[0], undefined, "major"); + pkg6.localDeps[0].localDeps.forEach((dep) => (dep._nextType = resolveReleaseType(dep, undefined, "major"))); + + expect(pkg6._nextType).toBe(undefined); + expect(pkg6._depsChanged.length).toBe(0); + expect(pkg6.localDeps[0]._nextType).toBe("major"); + expect(pkg6.localDeps[0]._depsChanged.length).toBe(2); + expect(pkg6.localDeps[0].localDeps[0]._nextType).toBe(false); + expect(pkg6.localDeps[0].localDeps[1]._nextType).toBe("patch"); + expect(pkg6.localDeps[0].localDeps[2]._nextType).toBe("minor"); + + //second round + pkg6._nextType = resolveReleaseType(pkg6, undefined, "major"); + pkg6.localDeps[0]._nextType = resolveReleaseType(pkg6.localDeps[0], undefined, "major"); + pkg6.localDeps[0].localDeps.forEach((dep) => (dep._nextType = resolveReleaseType(dep, undefined, "major"))); + + expect(pkg6._nextType).toBe("major"); + expect(pkg6._depsChanged.length).toBe(1); + expect(pkg6.localDeps[0]._nextType).toBe("major"); + expect(pkg6.localDeps[0]._depsChanged.length).toBe(2); + expect(pkg6.localDeps[0].localDeps[0]._nextType).toBe(false); + expect(pkg6.localDeps[0].localDeps[1]._nextType).toBe("patch"); + expect(pkg6.localDeps[0].localDeps[2]._nextType).toBe("minor"); + }); + + test("uses `patch` + override strategy as default (legacy flow)", () => { + const pkg6 = { + manifest: { dependencies: { a: "1.0.0" } }, + _nextType: undefined, + _lastRelease: { version: "1.0.0" }, + localDeps: [ + { + name: "a", + _nextType: false, + _lastRelease: { version: "1.0.0" }, + manifest: { dependencies: { b: "1.0.0", c: "1.0.0", d: "1.0.0" } }, + localDeps: [ + { name: "b", _nextType: false, localDeps: [], _lastRelease: { version: "1.0.0" } }, + { name: "c", _nextType: "minor", localDeps: [], _lastRelease: { version: "1.0.0" } }, + { name: "d", _nextType: "major", localDeps: [], _lastRelease: { version: "1.0.0" } }, + ], + }, + ], + }; + + //first round + pkg6._nextType = resolveReleaseType(pkg6); + pkg6.localDeps[0]._nextType = resolveReleaseType(pkg6.localDeps[0]); + pkg6.localDeps[0].localDeps.forEach((dep) => (dep._nextType = resolveReleaseType(dep))); + + expect(pkg6._nextType).toBe(undefined); + expect(pkg6._depsChanged.length).toBe(0); + expect(pkg6.localDeps[0]._nextType).toBe("patch"); + expect(pkg6.localDeps[0]._depsChanged.length).toBe(2); + expect(pkg6.localDeps[0].localDeps[0]._nextType).toBe(false); + expect(pkg6.localDeps[0].localDeps[1]._nextType).toBe("minor"); + expect(pkg6.localDeps[0].localDeps[2]._nextType).toBe("major"); + + //second round + pkg6._nextType = resolveReleaseType(pkg6); + pkg6.localDeps[0]._nextType = resolveReleaseType(pkg6.localDeps[0]); + pkg6.localDeps[0].localDeps.forEach((dep) => (dep._nextType = resolveReleaseType(dep))); + + expect(pkg6._nextType).toBe("patch"); + expect(pkg6._depsChanged.length).toBe(1); + expect(pkg6.localDeps[0]._nextType).toBe("patch"); + expect(pkg6.localDeps[0]._depsChanged.length).toBe(2); + expect(pkg6.localDeps[0].localDeps[0]._nextType).toBe(false); + expect(pkg6.localDeps[0].localDeps[1]._nextType).toBe("minor"); + expect(pkg6.localDeps[0].localDeps[2]._nextType).toBe("major"); }); }); describe("No infinite loops", () => { @@ -86,11 +215,22 @@ describe("resolveReleaseType()", () => { manifest: { dependencies: { a: "1.0.0" } }, }; pkg1.localDeps.push(pkg1); - expect(resolveReleaseType(pkg1)).toBe("minor"); + + expect(resolveReleaseType(pkg1, "override")).toBe("minor"); + expect(resolveReleaseType(pkg1, "satisfy")).toBe("minor"); + expect(resolveReleaseType(pkg1, "inherit")).toBe("minor"); + expect(resolveReleaseType(pkg1, "override", "patch")).toBe("minor"); + expect(resolveReleaseType(pkg1, "satisfy", "patch")).toBe("minor"); + expect(resolveReleaseType(pkg1, "inherit", "patch")).toBe("minor"); + + //strange case, because of self-reference, dependency gets updated thus releaseType becomes major + expect(resolveReleaseType(pkg1, "override", "major")).toBe("major"); + expect(resolveReleaseType(pkg1, "satisfy", "major")).toBe("major"); + expect(resolveReleaseType(pkg1, "inherit", "major")).toBe("major"); }); test("indirect self reference", () => { - const pkg1 = { + const pkga = { name: "a", _nextType: undefined, localDeps: [ @@ -105,10 +245,23 @@ describe("resolveReleaseType()", () => { _lastRelease: { version: "1.0.0" }, manifest: { dependencies: { b: "1.0.0" } }, }; - pkg1.localDeps[0].localDeps.push(pkg1); - pkg1.localDeps[0].manifest.dependencies = pkg1.manifest.dependencies; - expect(resolveReleaseType(pkg1)).toBe("patch"); + const pkgb = pkga.localDeps[0]; + pkgb.localDeps.push(pkga); + pkgb.manifest.dependencies = { a: "1.0.0" }; + + //multiple rounds! (1 too many actually) + for (let i = 0; i < 3; i++) { + pkga._nextType = resolveReleaseType(pkga); + pkgb._nextType = resolveReleaseType(pkgb); + } + + expect(pkga._nextType).toBe("patch"); + expect(pkga._depsChanged.length).toBe(1); + expect(pkga._depsChanged[0]).toBe(pkgb); + expect(pkgb._nextType).toBe("minor"); + expect(pkgb._depsChanged.length).toBe(1); + expect(pkgb._depsChanged[0]).toBe(pkga); }); test("new with self reference", () => { diff --git a/test/lib/updateDeps.test.js b/test/lib/updateDeps.test.js index 446c7275..ad9bbd5d 100644 --- a/test/lib/updateDeps.test.js +++ b/test/lib/updateDeps.test.js @@ -5,6 +5,7 @@ const { getNextPreVersion, getPreReleaseTag, getVersionFromTag, + updateManifestDeps, } = require("../../lib/updateDeps"); describe("resolveNextVersion()", () => { @@ -34,123 +35,6 @@ describe("resolveNextVersion()", () => { }); }); -describe("resolveReleaseType()", () => { - // prettier-ignore - const cases = [ - [ - "returns own package's _nextType if exists", - { - _nextType: "patch", - localDeps: [], - }, - undefined, - undefined, - "patch", - ], - [ - "implements `inherit` strategy: returns the highest release type of any deps", - { - manifest: { dependencies: { a: "1.0.0" } }, - _nextType: undefined, - _lastRelease: { version: "1.0.0" }, - localDeps: [ - { - name: "a", - manifest: { dependencies: { b: "1.0.0", c: "1.0.0", d: "1.0.0" } }, - _lastRelease: { version: "1.0.0" }, - _nextType: false, - localDeps: [ - { name: "b", _nextType: false, localDeps: [], _lastRelease: { version: "1.0.0" } }, - { name: "c", _nextType: "patch", localDeps: [], _lastRelease: { version: "1.0.0" } }, - { name: "d", _nextType: "major", localDeps: [], _lastRelease: { version: "1.0.0" } }, - ], - } - ] - }, - undefined, - "inherit", - "major" - ], - [ - "overrides dependent release type with custom value if defined", - { - manifest: { dependencies: { a: "1.0.0" } }, - _nextType: undefined, - _lastRelease: { version: "1.0.0" }, - localDeps: [ - { - name: "a", - _lastRelease: { version: "1.0.0" }, - manifest: { dependencies: { b: "1.0.0", c: "1.0.0", d: "1.0.0" } }, - _nextType: false, - localDeps: [ - { name: "b", _nextType: false, localDeps: [], _lastRelease: { version: "1.0.0" } }, - { name: "c", _nextType: "minor", localDeps: [], _lastRelease: { version: "1.0.0" } }, - { name: "d", _nextType: "patch", localDeps: [], _lastRelease: { version: "1.0.0" } }, - ], - }, - ], - }, - undefined, - "major", - "major" - ], - [ - "uses `patch` strategy as default (legacy flow)", - { - manifest: { dependencies: { a: "1.0.0" } }, - _nextType: undefined, - _lastRelease: { version: "1.0.0" }, - localDeps: [ - { - name: "a", - _nextType: false, - _lastRelease: { version: "1.0.0" }, - manifest: { dependencies: { b: "1.0.0", c: "1.0.0", d: "1.0.0" } }, - localDeps: [ - { name: "b", _nextType: false, localDeps: [], _lastRelease: { version: "1.0.0" } }, - { name: "c", _nextType: "minor", localDeps: [], _lastRelease: { version: "1.0.0" } }, - { name: "d", _nextType: "major", localDeps: [], _lastRelease: { version: "1.0.0" } }, - ], - }, - ], - }, - undefined, - undefined, - "patch" - ], - [ - "returns undefined if no _nextRelease found", - { - _nextType: undefined, - localDeps: [ - { - _nextType: false, - localDeps: [ - { _nextType: false, localDeps: [] }, - { - _nextType: undefined, - localDeps: [ - { _nextType: undefined, localDeps: [] } - ] - }, - ], - }, - ], - }, - undefined, - undefined, - undefined, - ], - ] - - cases.forEach(([name, pkg, bumpStrategy, releaseStrategy, result]) => { - it(name, () => { - expect(resolveReleaseType(pkg, bumpStrategy, releaseStrategy)).toBe(result); - }); - }); -}); - describe("getNextVersion()", () => { // prettier-ignore const cases = [ @@ -264,3 +148,99 @@ describe("getVersionFromTag()", () => { }); }); }); + +describe("updateManifestDeps()", () => { + test("updates all changed dependencies to next version", () => { + const pkg = { + name: "a", + _lastRelease: { version: "1.0.0" }, + _nextType: false, + manifest: { name: "a", dependencies: { b: "1.0.0", c: "1.0.0", d: "1.0.0" } }, + localDeps: [ + { name: "b", _nextType: false, localDeps: [], _lastRelease: { version: "1.0.0" } }, + { + name: "c", + _nextType: "patch", + localDeps: [], + _lastRelease: { version: "1.0.0" }, + _nextRelease: { version: "1.0.1" }, + }, + { + name: "d", + _nextType: "major", + localDeps: [], + _lastRelease: { version: "1.0.0" }, + _nextRelease: { version: "2.0.0" }, + }, + ], + }; + pkg._depsChanged = [pkg.localDeps[1], pkg.localDeps[2]]; + + updateManifestDeps(pkg, false); + expect(pkg.manifest).toMatchObject({ name: "a", dependencies: { b: "1.0.0", c: "1.0.1", d: "2.0.0" } }); + }); + + test("updates dependency with existing version", () => { + const pkg = { + name: "a", + _lastRelease: { version: "1.0.0" }, + _nextType: false, + manifest: { name: "a", dependencies: { b: "*", c: "1.0.0", d: "1.0.0" } }, + localDeps: [ + { name: "b", _nextType: false, localDeps: [], _lastRelease: { version: "1.0.0" } }, + { + name: "c", + _nextType: "patch", + localDeps: [], + _lastRelease: { version: "1.0.0" }, + _nextRelease: { version: "1.0.1" }, + }, + { + name: "d", + _nextType: "major", + localDeps: [], + _lastRelease: { version: "1.0.0" }, + _nextRelease: { version: "2.0.0" }, + }, + ], + }; + pkg._depsChanged = pkg.localDeps; + + updateManifestDeps(pkg, false); + expect(pkg.manifest).toMatchObject({ name: "a", dependencies: { b: "1.0.0", c: "1.0.1", d: "2.0.0" } }); + }); + + test("updates dependencies if mentioned twice", () => { + const pkg = { + name: "a", + _lastRelease: { version: "1.0.0" }, + _nextType: false, + manifest: { name: "a", dependencies: { b: "*", c: "1.0.0", d: "1.0.0" }, devDependencies: { c: "1.0.0" } }, + localDeps: [ + { name: "b", _nextType: false, localDeps: [], _lastRelease: { version: "1.0.0" } }, + { + name: "c", + _nextType: "patch", + localDeps: [], + _lastRelease: { version: "1.0.0" }, + _nextRelease: { version: "1.0.1" }, + }, + { + name: "d", + _nextType: "major", + localDeps: [], + _lastRelease: { version: "1.0.0" }, + _nextRelease: { version: "2.0.0" }, + }, + ], + }; + pkg._depsChanged = pkg.localDeps; + + updateManifestDeps(pkg, false); + expect(pkg.manifest).toMatchObject({ + name: "a", + dependencies: { b: "1.0.0", c: "1.0.1", d: "2.0.0" }, + devDependencies: { c: "1.0.1" }, + }); + }); +});