Skip to content

Commit 2d083e0

Browse files
authored
fix: handle pnpm monorepo workspace:^ version correctly (#111) (#128)
* fix: update resolveNextVersion to handle pnpm workspace: versions * fix: updateManifestDeps now use dep version in package.json (instead of release version) to determine next dep version * refactor: use regex to substitude version with workspace * test: add integration tests with yarn workspace ranges closes #111
1 parent 2dedffc commit 2d083e0

File tree

9 files changed

+231
-10
lines changed

9 files changed

+231
-10
lines changed

lib/updateDeps.js

+27-5
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,9 @@ const manifestUpdateNecessary = (scope, name, nextVersion, bumpStrategy, prefix)
241241
* @internal
242242
*/
243243
const resolveNextVersion = (currentVersion, nextVersion, bumpStrategy = "override", prefix = "") => {
244+
// handle cases of "workspace protocol" defined in yarn and pnpm workspace, whose version starts with "workspace:"
245+
currentVersion = substituteWorkspaceVersion(currentVersion, nextVersion);
246+
244247
//no change...
245248
if (currentVersion === nextVersion) return currentVersion;
246249

@@ -260,10 +263,10 @@ const resolveNextVersion = (currentVersion, nextVersion, bumpStrategy = "overrid
260263
const currentChunks = currentVersion.split(sep);
261264
// prettier-ignore
262265
const resolvedChunks = currentChunks.map((chunk, i) =>
263-
nextChunks[i]
264-
? chunk.replace(/\d+/, nextChunks[i])
265-
: chunk
266-
);
266+
nextChunks[i]
267+
? chunk.replace(/\d+/, nextChunks[i])
268+
: chunk
269+
);
267270

268271
return resolvedChunks.join(sep);
269272
}
@@ -273,6 +276,25 @@ const resolveNextVersion = (currentVersion, nextVersion, bumpStrategy = "overrid
273276
return prefix + nextVersion;
274277
};
275278

279+
/**
280+
* Substitute "workspace:" in currentVersion
281+
* See:
282+
* {@link https://yarnpkg.com/features/workspaces#publishing-workspaces}
283+
* {@link https://pnpm.io/workspaces#publishing-workspace-packages}
284+
*
285+
* @param {string} currentVersion Current version, may start with "workspace:"
286+
* @param {string} nextVersion Next version
287+
* @returns {string} current version without "workspace:"
288+
*/
289+
const substituteWorkspaceVersion = (currentVersion, nextVersion) => {
290+
if (currentVersion.startsWith("workspace:")) {
291+
const [, range, caret] = /^workspace:(([\^~*])?.*)$/.exec(currentVersion);
292+
293+
return caret === range ? (caret === "*" ? nextVersion : caret + nextVersion) : range;
294+
}
295+
return currentVersion;
296+
};
297+
276298
/**
277299
* Update pkg deps.
278300
*
@@ -306,7 +328,7 @@ const updateManifestDeps = (pkg, writeOut = true, bumpStrategy = "override", pre
306328
scopes.forEach((scope) => {
307329
if (scope[dependency.name]) {
308330
scope[dependency.name] = resolveNextVersion(
309-
get(dependency, "_lastRelease.version", "0.0.0"),
331+
scope[dependency.name],
310332
release.version,
311333
bumpStrategy,
312334
prefix
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "msr-test-yarn",
3+
"author": "Dave Houlbrooke <[email protected]>",
4+
"version": "0.0.0-semantically-released",
5+
"private": true,
6+
"license": "0BSD",
7+
"engines": {
8+
"node": ">=8.3"
9+
},
10+
"workspaces": [
11+
"packages/*"
12+
],
13+
"release": {
14+
"plugins": [
15+
"@semantic-release/commit-analyzer",
16+
"@semantic-release/release-notes-generator"
17+
],
18+
"noCi": true
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "msr-test-a",
3+
"version": "0.0.0",
4+
"dependencies": {
5+
"msr-test-b": "workspace:*"
6+
},
7+
"devDependencies": {
8+
"msr-test-c": "workspace:^"
9+
},
10+
"peerDependencies": {
11+
"msr-test-d": "workspace:~",
12+
"left-pad": "latest"
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "msr-test-b",
3+
"version": "0.0.0",
4+
"optionalDependencies": {
5+
"msr-test-d": "workspace:^0.0.0"
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"tagFormat": "multi-semantic-release-test-c@v${version}"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "msr-test-c",
3+
"version": "0.0.0"
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "msr-test-d",
3+
"version": "0.0.0"
4+
}

test/lib/multiSemanticRelease.test.js

+129-5
Original file line numberDiff line numberDiff line change
@@ -1729,21 +1729,145 @@ describe("multiSemanticRelease()", () => {
17291729
// Check manifests.
17301730
expect(require(`${cwd}/packages/a/package.json`)).toMatchObject({
17311731
peerDependencies: {
1732-
"msr-test-c": strategy === "inherit" ? "1.0.0" : prefix + "1.0.0",
1732+
"msr-test-c": strategy === "inherit" ? "^1.0.0" : prefix + "1.0.0",
17331733
},
17341734
});
17351735
expect(require(`${cwd}/packages/b/package.json`)).toMatchObject({
17361736
dependencies: {
1737-
"msr-test-a": strategy === "inherit" ? "1.0.0" : prefix + "1.0.0",
1737+
"msr-test-a": strategy === "inherit" ? "^1.0.0" : prefix + "1.0.0",
17381738
},
17391739
devDependencies: {
1740-
"msr-test-c": strategy === "inherit" ? "1.0.0" : prefix + "1.0.0",
1740+
"msr-test-c": strategy === "inherit" ? "^1.0.0" : prefix + "1.0.0",
17411741
},
17421742
});
17431743
expect(require(`${cwd}/packages/c/package.json`)).toMatchObject({
17441744
devDependencies: {
1745-
"msr-test-b": strategy === "inherit" ? "1.0.0" : prefix + "1.0.0",
1746-
"msr-test-d": strategy === "inherit" ? "1.0.0" : prefix + "1.0.0",
1745+
"msr-test-b": strategy === "inherit" ? "^1.0.0" : prefix + "1.0.0",
1746+
"msr-test-d": strategy === "inherit" ? "^1.0.0" : prefix + "1.0.0",
1747+
},
1748+
});
1749+
});
1750+
});
1751+
describe.each([
1752+
["override", "^"],
1753+
["satisfy", "^"],
1754+
["inherit", "^"],
1755+
])("With Yarn Workspace Ranges & deps.bump=%s & deps.prefix=%s", (strategy, prefix) => {
1756+
test('should replace "workspace:" with correct version', async () => {
1757+
// Create Git repo with copy of Yarn workspaces fixture.
1758+
const cwd = gitInit();
1759+
copyDirectory(`test/fixtures/yarnWorkspacesRanges/`, cwd);
1760+
const sha = gitCommitAll(cwd, "feat: Initial release");
1761+
gitInitOrigin(cwd);
1762+
gitPush(cwd);
1763+
1764+
// Capture output.
1765+
const stdout = new WritableStreamBuffer();
1766+
const stderr = new WritableStreamBuffer();
1767+
1768+
// Call multiSemanticRelease()
1769+
// Doesn't include plugins that actually publish.
1770+
const multiSemanticRelease = require("../../");
1771+
const result = await multiSemanticRelease(
1772+
[
1773+
`packages/a/package.json`,
1774+
`packages/b/package.json`,
1775+
`packages/c/package.json`,
1776+
`packages/d/package.json`,
1777+
],
1778+
{},
1779+
{ cwd, stdout, stderr },
1780+
{ deps: { bump: strategy, prefix } }
1781+
);
1782+
1783+
// Get stdout and stderr output.
1784+
const err = stderr.getContentsAsString("utf8");
1785+
expect(err).toBe(false);
1786+
const out = stdout.getContentsAsString("utf8");
1787+
expect(out).toMatch("Started multirelease! Loading 4 packages...");
1788+
expect(out).toMatch("Loaded package msr-test-a");
1789+
expect(out).toMatch("Loaded package msr-test-b");
1790+
expect(out).toMatch("Loaded package msr-test-c");
1791+
expect(out).toMatch("Loaded package msr-test-d");
1792+
expect(out).toMatch("Queued 4 packages! Starting release...");
1793+
expect(out).toMatch("Created tag [email protected]");
1794+
expect(out).toMatch("Created tag [email protected]");
1795+
expect(out).toMatch("Created tag [email protected]");
1796+
expect(out).toMatch("Created tag [email protected]");
1797+
expect(out).toMatch("Released 4 of 4 packages, semantically!");
1798+
1799+
// A.
1800+
expect(result[0].name).toBe("msr-test-a");
1801+
expect(result[0].result.lastRelease).toEqual({});
1802+
expect(result[0].result.nextRelease).toMatchObject({
1803+
gitHead: sha,
1804+
gitTag: "[email protected]",
1805+
type: "minor",
1806+
version: "1.0.0",
1807+
});
1808+
expect(result[0].result.nextRelease.notes).toMatch("# msr-test-a 1.0.0");
1809+
expect(result[0].result.nextRelease.notes).toMatch("### Features\n\n* Initial release");
1810+
expect(result[0].result.nextRelease.notes).toMatch(
1811+
"### Dependencies\n\n* **msr-test-b:** upgraded to 1.0.0\n* **msr-test-c:** upgraded to 1.0.0"
1812+
);
1813+
1814+
// B.
1815+
expect(result[1].name).toBe("msr-test-b");
1816+
expect(result[1].result.lastRelease).toEqual({});
1817+
expect(result[1].result.nextRelease).toMatchObject({
1818+
gitHead: sha,
1819+
gitTag: "[email protected]",
1820+
type: "minor",
1821+
version: "1.0.0",
1822+
});
1823+
expect(result[1].result.nextRelease.notes).toMatch("# msr-test-b 1.0.0");
1824+
expect(result[1].result.nextRelease.notes).toMatch("### Features\n\n* Initial release");
1825+
expect(result[1].result.nextRelease.notes).toMatch(
1826+
"### Dependencies\n\n* **msr-test-d:** upgraded to 1.0.0"
1827+
);
1828+
1829+
// C.
1830+
expect(result[2].name).toBe("msr-test-c");
1831+
expect(result[2].result.lastRelease).toEqual({});
1832+
expect(result[2].result.nextRelease).toMatchObject({
1833+
gitHead: sha,
1834+
gitTag: "[email protected]",
1835+
type: "minor",
1836+
version: "1.0.0",
1837+
});
1838+
expect(result[2].result.nextRelease.notes).toMatch("# msr-test-c 1.0.0");
1839+
expect(result[2].result.nextRelease.notes).toMatch("### Features\n\n* Initial release");
1840+
1841+
// D.
1842+
expect(result[3].name).toBe("msr-test-d");
1843+
expect(result[3].result.lastRelease).toEqual({});
1844+
expect(result[3].result.nextRelease).toMatchObject({
1845+
gitHead: sha,
1846+
gitTag: "[email protected]",
1847+
type: "minor",
1848+
version: "1.0.0",
1849+
});
1850+
expect(result[3].result.nextRelease.notes).toMatch("# msr-test-d 1.0.0");
1851+
expect(result[3].result.nextRelease.notes).toMatch("### Features\n\n* Initial release");
1852+
1853+
// ONLY four times.
1854+
expect(result).toHaveLength(4);
1855+
1856+
// Check manifests.
1857+
expect(require(`${cwd}/packages/a/package.json`)).toMatchObject({
1858+
dependencies: {
1859+
"msr-test-b": "1.0.0",
1860+
},
1861+
devDependencies: {
1862+
"msr-test-c": strategy === "override" ? prefix + "1.0.0" : "^1.0.0",
1863+
},
1864+
peerDependencies: {
1865+
"msr-test-d": strategy === "override" ? prefix + "1.0.0" : "~1.0.0",
1866+
},
1867+
});
1868+
expect(require(`${cwd}/packages/b/package.json`)).toMatchObject({
1869+
optionalDependencies: {
1870+
"msr-test-d": strategy === "override" ? prefix + "1.0.0" : "^1.0.0",
17471871
},
17481872
});
17491873
});

test/lib/updateDeps.test.js

+23
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,29 @@ describe("resolveNextVersion()", () => {
2626
["*", "2.0.0", "inherit", "*"],
2727
["~1.0", "2.0.0", "inherit", "~2.0"],
2828
["~2.0", "2.1.0", "inherit", "~2.1"],
29+
30+
// cases of "workspace protocol" defined in yarn and pnpm
31+
["workspace:*", "1.0.1", undefined, "1.0.1"],
32+
["workspace:*", "1.0.1", "override", "1.0.1"],
33+
34+
["workspace:*", "1.3.0", "satisfy", "1.3.0"],
35+
["workspace:~", "1.0.1", "satisfy", "~1.0.1"],
36+
["workspace:^", "1.3.0", "satisfy", "^1.3.0"],
37+
// the following cases should be treated as if "workspace:" was removed
38+
["workspace:^1.0.0", "1.0.1", "satisfy", "^1.0.0"],
39+
["workspace:^1.2.0", "1.3.0", "satisfy", "^1.2.0"],
40+
["workspace:1.2.x", "1.2.2", "satisfy", "1.2.x"],
41+
42+
["workspace:*", "1.3.0", "inherit", "1.3.0"],
43+
["workspace:~", "1.1.0", "inherit", "~1.1.0"],
44+
["workspace:^", "2.0.0", "inherit", "^2.0.0"],
45+
// the following cases should be treated as if "workspace:" was removed
46+
["workspace:~1.0.0", "1.1.0", "inherit", "~1.1.0"],
47+
["workspace:1.2.x", "1.2.1", "inherit", "1.2.x"],
48+
["workspace:1.2.x", "1.3.0", "inherit", "1.3.x"],
49+
["workspace:^1.0.0", "2.0.0", "inherit", "^2.0.0"],
50+
["workspace:~1.0", "2.0.0", "inherit", "~2.0"],
51+
["workspace:~2.0", "2.1.0", "inherit", "~2.1"],
2952
]
3053

3154
cases.forEach(([currentVersion, nextVersion, strategy, resolvedVersion]) => {

0 commit comments

Comments
 (0)