Skip to content

Commit e77905d

Browse files
authored
feat: provide ignore packages CLI directive
1 parent a034cb3 commit e77905d

30 files changed

+817
-71
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
/node_modules/
1+
**/node_modules/**
22
/coverage/
33
*.log

README.md

+37
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,32 @@ _But_ in multi-semantic-release this configuration can be done globally (in your
3232

3333
multi-semantic-release does not support any command line arguments (this wasn't possible without duplicating files from semantic-release, which I've tried to avoid).
3434

35+
Make sure to have a `workspaces` attribute inside your `package.json` project file. In there, you can set a list of packages that you might want to process in the msr process, as well as ignore others. For example, let's say your project has 4 packages (i.e. a, b, c and d) and you want to process only a and d (ignore b and c). You can set the following structure in your `package.json` file:
36+
```json
37+
{
38+
"name": "msr-test-yarn",
39+
"author": "Dave Houlbrooke <[email protected]",
40+
"version": "0.0.0-semantically-released",
41+
"private": true,
42+
"license": "0BSD",
43+
"engines": {
44+
"node": ">=8.3"
45+
},
46+
"workspaces": [
47+
"packages/*",
48+
"!packages/b/**",
49+
"!packages/c/**"
50+
],
51+
"release": {
52+
"plugins": [
53+
"@semantic-release/commit-analyzer",
54+
"@semantic-release/release-notes-generator"
55+
],
56+
"noCi": true
57+
}
58+
}
59+
```
60+
3561
## CLI
3662
There are several tweaks to adapt **msr** to some corner cases:
3763

@@ -43,6 +69,17 @@ There are several tweaks to adapt **msr** to some corner cases:
4369
|`--deps.bump`|string| Define deps version update rule. `override` — replace any prev version with the next one, `satisfy` — 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; `inherit` will try to follow the current declaration version/range. `~1.0.0` + `minor` turns into `~1.1.0`, `1.x` + `major` gives `2.x`, but `1.x` + `minor` gives `1.x` so there will be no release, etc. + **Experimental feat** | `override`
4470
|`--deps.release`|string| 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. **Experimental feat** | `patch`
4571
|`--dry-run`|bool |Dry run mode| `false`
72+
|`--ignore-packages`|string|Packages list to be ignored on bumping process (append to the ones that already exist at package.json workspaces)|`null`
73+
74+
Examples:
75+
76+
```
77+
$ multi-semantic-release --debug
78+
$ multi-semantic-release --deps.bump=satisfy --deps.release=patch
79+
$ multi-semantic-release --ignore-packages=packages/a/**,packages/b/**
80+
```
81+
82+
You can also combine the CLI `--ignore-packages` options with the `!` operator at each package inside `package.json.workspaces` attribute. Even though you can use the CLI to ignore options, you can't use it to set which packages to be released – i.e. you still need to set the `workspaces` attribute inside the `package.json`.
4683

4784
## API
4885

bin/cli.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ const cli = meow(
1515
--first-parent Apply commit filtering to current branch only.
1616
--deps.bump Define deps version updating rule. Allowed: override, satisfy, inherit.
1717
--deps.release Define release type for dependent package if any of its deps changes. Supported values: patch, minor, major, inherit.
18+
--ignore-packages Packages' list to be ignored on bumping process
1819
--help Help info.
1920
2021
Examples
2122
$ multi-semantic-release --debug
2223
$ multi-semantic-release --deps.bump=satisfy --deps.release=patch
24+
$ multi-semantic-release --ignore-packages=packages/a/**,packages/b/**
2325
`,
2426
{
2527
flags: {
@@ -40,13 +42,21 @@ const cli = meow(
4042
type: "string",
4143
default: "patch",
4244
},
45+
ignorePackages: {
46+
type: "string",
47+
},
4348
dryRun: {
4449
type: "boolean",
4550
},
4651
},
4752
}
4853
);
4954

50-
const processFlags = (flags) => toPairs(flags).reduce((m, [k, v]) => set(m, k, v), {});
55+
const processFlags = (flags) => {
56+
return toPairs(flags).reduce((m, [k, v]) => {
57+
if (k === "ignorePackages" && v) return set(m, k, v.split(","));
58+
return set(m, k, v);
59+
}, {});
60+
};
5161

5262
runner(processFlags(cli.flags));

bin/runner.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ module.exports = (flags) => {
1919
console.log(`flags: ${JSON.stringify(flags, null, 2)}`);
2020

2121
// Get list of package.json paths according to Yarn workspaces.
22-
const paths = getWorkspacesYarn(cwd);
22+
const paths = getWorkspacesYarn(cwd, flags.ignorePackages);
2323
console.log("yarn paths", paths);
2424

2525
// Do multirelease (log out any errors).

lib/createInlinePluginCreator.js

+1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ function createInlinePluginCreator(packages, multiContext, synchronizer, flags)
8787
const analyzeCommits = async (pluginOptions, context) => {
8888
const firstParentBranch = flags.firstParent ? context.branch.name : undefined;
8989
pkg._preRelease = context.branch.prerelease || null;
90+
pkg._branch = context.branch.name;
9091

9192
// Filter commits by directory.
9293
commits = await getCommitsFiltered(cwd, dir, context.lastRelease.gitHead, firstParentBranch);

lib/getWorkspacesYarn.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ const { checker } = require("./blork");
66
* Return array of package.json for Yarn workspaces.
77
*
88
* @param {string} cwd The current working directory where a package.json file can be found.
9+
* @param {string[]|null} ignorePackages (Optional) Packages to be ignored passed via cli.
910
* @returns {string[]} An array of package.json files corresponding to the workspaces setting in package.json
1011
*/
11-
function getWorkspacesYarn(cwd) {
12+
function getWorkspacesYarn(cwd, ignorePackages = null) {
1213
// Load package.json
1314
const manifest = getManifest(`${cwd}/package.json`);
1415

@@ -22,13 +23,16 @@ function getWorkspacesYarn(cwd) {
2223
throw new TypeError("package.json: workspaces or workspaces.packages: Must be non-empty array of string");
2324
}
2425

26+
// If packages to be ignored come from CLI, we need to combine them with the ones from manifest workspaces
27+
if (Array.isArray(ignorePackages)) packages.push(...ignorePackages.map((p) => `!${p}`));
28+
2529
// Turn workspaces into list of package.json files.
2630
const workspaces = glob(
2731
packages.map((p) => p.replace(/\/?$/, "/package.json")),
2832
{
2933
cwd: cwd,
30-
realpath: true,
31-
ignore: "**/node_modules/**",
34+
absolute: true,
35+
gitignore: true,
3236
}
3337
);
3438

lib/git.js

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const execa = require("execa");
2+
3+
/**
4+
* Get all the tags for a given branch.
5+
*
6+
* @param {String} branch The branch for which to retrieve the tags.
7+
* @param {Object} [execaOptions] Options to pass to `execa`.
8+
* @param {Array<String>} filters List of prefixes/sufixes to be checked inside tags.
9+
*
10+
* @return {Array<String>} List of git tags.
11+
* @throws {Error} If the `git` command fails.
12+
* @internal
13+
*/
14+
function getTags(branch, execaOptions, filters) {
15+
let tags = execa.sync("git", ["tag", "--merged", branch], execaOptions).stdout;
16+
tags = tags
17+
.split("\n")
18+
.map((tag) => tag.trim())
19+
.filter(Boolean);
20+
21+
if (!filters || !filters.length) return tags;
22+
23+
const validateSubstr = (t, f) => !!f.find((v) => t.includes(v));
24+
25+
return tags.filter((tag) => validateSubstr(tag, filters));
26+
}
27+
28+
module.exports = {
29+
getTags,
30+
};

lib/glob.js

+3-6
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
const bashGlob = require("bash-glob");
2-
const bashPath = require("bash-path");
1+
const globby = require("globby");
32

43
module.exports = (...args) => {
5-
if (!bashPath) {
6-
throw new TypeError("`bash` must be installed"); // TODO move this check to bash-glob
7-
}
4+
const [pattern, ...options] = args;
85

9-
return bashGlob.sync(...args);
6+
return globby.sync(pattern, ...options);
107
};

lib/updateDeps.js

+144-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
const { writeFileSync } = require("fs");
2-
const recognizeFormat = require("./recognizeFormat");
32
const semver = require("semver");
3+
const { isObject, isEqual, transform } = require("lodash");
4+
const recognizeFormat = require("./recognizeFormat");
5+
const getManifest = require("./getManifest");
6+
const { getHighestVersion, getLatestVersion } = require("./utils");
7+
const { getTags } = require("./git");
8+
const debug = require("debug")("msr:updateDeps");
49

510
/**
611
* Resolve next package version.
@@ -17,17 +22,106 @@ const getNextVersion = (pkg) => {
1722
: lastVersion || "1.0.0";
1823
};
1924

25+
/**
26+
* Resolve the package version from a tag
27+
*
28+
* @param {Package} pkg Package object.
29+
* @param {string} tag The tag containing the version to resolve
30+
* @returns {string} The version of the package
31+
* @returns {string|null} The version of the package or null if no tag was passed
32+
* @internal
33+
*/
34+
const getVersionFromTag = (pkg, tag) => {
35+
if (!pkg.name) return tag || null;
36+
if (!tag) return null;
37+
38+
const strMatch = tag.match(/[0-9].[0-9].[0-9].*/);
39+
return strMatch && strMatch[0] && semver.valid(strMatch[0]) ? strMatch[0] : null;
40+
};
41+
2042
/**
2143
* Resolve next package version on prereleases.
2244
*
2345
* @param {Package} pkg Package object.
46+
* @param {Array<string>} tags Override list of tags from specific pkg and branch.
2447
* @returns {string|undefined} Next pkg version.
2548
* @internal
2649
*/
27-
const getNextPreVersion = (pkg) => {
50+
const getNextPreVersion = (pkg, tags) => {
51+
const tagFilters = [pkg._preRelease];
2852
const lastVersion = pkg._lastRelease && pkg._lastRelease.version;
53+
// Extract tags:
54+
// 1. Set filter to extract only package tags
55+
// 2. Get tags from a branch considering the filters established
56+
// 3. Resolve the versions from the tags
57+
// TODO: replace {cwd: '.'} with multiContext.cwd
58+
if (pkg.name) tagFilters.push(pkg.name);
59+
if (!tags || !tags.length) {
60+
tags = getTags(pkg._branch, { cwd: "." }, tagFilters);
61+
}
62+
const lastPreRelTag = getPreReleaseTag(lastVersion);
63+
const isNewPreRelTag = lastPreRelTag && lastPreRelTag !== pkg._preRelease;
64+
const versionToSet =
65+
isNewPreRelTag || !lastVersion
66+
? `1.0.0-${pkg._preRelease}.1`
67+
: _nextPreVersionCases(
68+
tags.map((tag) => getVersionFromTag(pkg, tag)).filter((tag) => tag),
69+
lastVersion,
70+
pkg._nextType,
71+
pkg._preRelease
72+
);
73+
return versionToSet;
74+
};
75+
76+
/**
77+
* Parse the prerelease tag from a semver version.
78+
*
79+
* @param {string} version Semver version in a string format.
80+
* @returns {string|null} preReleaseTag Version prerelease tag or null.
81+
* @internal
82+
*/
83+
const getPreReleaseTag = (version) => {
84+
const parsed = semver.parse(version);
85+
if (!parsed) return null;
86+
return parsed.prerelease[0] || null;
87+
};
88+
89+
/**
90+
* Resolve next prerelease special cases: highest version from tags or major/minor/patch.
91+
*
92+
* @param {Array} tags List of all released tags from package.
93+
* @param {string} lastVersion Last package version released.
94+
* @param {string} pkgNextType Next type evaluated for the next package type.
95+
* @param {string} pkgPreRelease Package prerelease suffix.
96+
* @returns {string|undefined} Next pkg version.
97+
* @internal
98+
*/
99+
const _nextPreVersionCases = (tags, lastVersion, pkgNextType, pkgPreRelease) => {
100+
// Case 1: Normal release on last version and is now converted to a prerelease
101+
if (!semver.prerelease(lastVersion)) {
102+
const { major, minor, patch } = semver.parse(lastVersion);
103+
return `${semver.inc(`${major}.${minor}.${patch}`, pkgNextType || "patch")}-${pkgPreRelease}.1`;
104+
}
29105

30-
return lastVersion ? semver.inc(lastVersion, "prerelease", pkg._preRelease) : `1.0.0-${pkg._preRelease}.1`;
106+
// Case 2: Validates version with tags
107+
const latestTag = getLatestVersion(tags, { withPrerelease: true });
108+
return _nextPreHighestVersion(latestTag, lastVersion, pkgPreRelease);
109+
};
110+
111+
/**
112+
* Resolve next prerelease comparing bumped tags versions with last version.
113+
*
114+
* @param {string|null} latestTag Last released tag from branch or null if non-existent.
115+
* @param {string} lastVersion Last version released.
116+
* @param {string} pkgPreRelease Prerelease tag from package to-be-released.
117+
* @returns {string} Next pkg version.
118+
* @internal
119+
*/
120+
const _nextPreHighestVersion = (latestTag, lastVersion, pkgPreRelease) => {
121+
const bumpFromTags = latestTag ? semver.inc(latestTag, "prerelease", pkgPreRelease) : null;
122+
const bumpFromLast = semver.inc(lastVersion, "prerelease", pkgPreRelease);
123+
124+
return bumpFromTags ? getHighestVersion(bumpFromLast, bumpFromTags) : bumpFromLast;
31125
};
32126

33127
/**
@@ -181,14 +275,61 @@ const updateManifestDeps = (pkg) => {
181275
throw Error(`Cannot release because dependency ${d.name} has not been released`);
182276
});
183277

278+
if (!auditManifestChanges(manifest, path)) {
279+
return;
280+
}
281+
184282
// Write package.json back out.
185283
writeFileSync(path, JSON.stringify(manifest, null, indent) + trailingWhitespace);
186284
};
187285

286+
// https://gist.github.com/Yimiprod/7ee176597fef230d1451
287+
const difference = (object, base) =>
288+
transform(object, (result, value, key) => {
289+
if (!isEqual(value, base[key])) {
290+
result[key] =
291+
isObject(value) && isObject(base[key]) ? difference(value, base[key]) : `${base[key]}${value}`;
292+
}
293+
});
294+
295+
/**
296+
* Clarify what exactly was changed in manifest file.
297+
* @param {object} actualManifest manifest object
298+
* @param {string} path manifest path
299+
* @returns {boolean} has changed or not
300+
* @internal
301+
*/
302+
const auditManifestChanges = (actualManifest, path) => {
303+
const debugPrefix = `[${actualManifest.name}]`;
304+
const oldManifest = getManifest(path);
305+
const depScopes = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
306+
const changes = depScopes.reduce((res, scope) => {
307+
const diff = difference(actualManifest[scope], oldManifest[scope]);
308+
309+
if (Object.keys(diff).length) {
310+
res[scope] = diff;
311+
}
312+
313+
return res;
314+
}, {});
315+
316+
debug(debugPrefix, "package.json path=", path);
317+
318+
if (Object.keys(changes).length) {
319+
debug(debugPrefix, "changes=", changes);
320+
return true;
321+
}
322+
323+
debug(debugPrefix, "no deps changes");
324+
return false;
325+
};
326+
188327
module.exports = {
189328
getNextVersion,
190329
getNextPreVersion,
330+
getPreReleaseTag,
191331
updateManifestDeps,
192332
resolveReleaseType,
193333
resolveNextVersion,
334+
getVersionFromTag,
194335
};

0 commit comments

Comments
 (0)