Skip to content

Commit

Permalink
perf: up deps, minor code improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
antongolub committed Nov 23, 2020
1 parent a269628 commit a7aa625
Show file tree
Hide file tree
Showing 12 changed files with 162 additions and 102 deletions.
4 changes: 4 additions & 0 deletions bin/cli.js
Expand Up @@ -9,6 +9,7 @@ const cli = meow(
$ multi-semantic-release
Options
--dry-run Dry run mode.
--debug Output debugging information.
--sequential-init Avoid hypothetical concurrent initialization collisions.
--first-parent Apply commit filtering to current branch only.
Expand Down Expand Up @@ -39,6 +40,9 @@ const cli = meow(
type: "string",
default: "patch",
},
dryRun: {
type: "boolean",
},
},
}
);
Expand Down
52 changes: 31 additions & 21 deletions lib/createInlinePluginCreator.js
Expand Up @@ -30,7 +30,15 @@ function createInlinePluginCreator(packages, multiContext, synchronizer, flags)
*/
function createInlinePlugin(pkg) {
// Vars.
const { deps, plugins, dir, name } = pkg;
const { plugins, dir, name } = pkg;
const next = () => {
pkg._tagged = true;

emit(
"_readyForTagging",
todo().find((p) => p._nextType && !p._tagged)
);
};

/**
* @var {Commit[]} List of _filtered_ commits that only apply to this package.
Expand Down Expand Up @@ -88,10 +96,6 @@ function createInlinePluginCreator(packages, multiContext, synchronizer, flags)
// Set lastRelease for package from context.
pkg._lastRelease = context.lastRelease;

// Make a list of local dependencies.
// Map dependency names (e.g. my-awesome-dep) to their actual package objects in the packages array.
pkg._localDeps = deps.map((d) => packages.find((p) => d === p.name)).filter(Boolean);

// Set nextType for package from plugins.
pkg._nextType = await plugins.analyzeCommits(context);

Expand Down Expand Up @@ -142,14 +146,6 @@ function createInlinePluginCreator(packages, multiContext, synchronizer, flags)
// Wait until all todo packages are ready to generate notes.
await waitForAll("_nextRelease", (p) => p._nextType);

// Wait until the current pkg is ready to generate notes
getLucky("_readyToGenerateNotes", pkg);
await waitFor("_readyToGenerateNotes", pkg);

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

// Vars.
const notes = [];

Expand All @@ -164,7 +160,7 @@ function createInlinePluginCreator(packages, multiContext, synchronizer, flags)
if (subs) notes.push(subs.replace(/^(#+) (\[?\d+\.\d+\.\d+\]?)/, `$1 ${name} $2`));

// If it has upgrades add an upgrades section.
const upgrades = pkg._localDeps.filter((d) => d._nextRelease);
const upgrades = pkg.localDeps.filter((d) => d._nextRelease);
if (upgrades.length) {
notes.push(`### Dependencies`);
const bullets = upgrades.map((d) => `* **${d.name}:** upgraded to ${d._nextRelease.version}`);
Expand All @@ -173,22 +169,35 @@ function createInlinePluginCreator(packages, multiContext, synchronizer, flags)

debug("notes generated: %s", pkg.name);

if (pkg.options.dryRun) {
next();
}

// Return the notes.
return notes.join("\n\n");
};

const publish = async (pluginOptions, context) => {
const prepare = async (pluginOptions, context) => {
// Wait until the current pkg is ready to be tagged
getLucky("_readyForTagging", pkg);
await waitFor("_readyForTagging", pkg);

const res = await plugins.prepare(context);
pkg._prepared = true;

emit(
"_readyToGenerateNotes",
todo().find((p) => p._nextType && !p._prepared)
);
updateManifestDeps(pkg);
pkg._depsUpdated = true;

// Wait for all packages to be `prepare`d and tagged by `semantic-release`
await waitForAll("_prepared", (p) => p._nextType);
debug("prepared: %s", pkg.name);

return res;
};

const publish = async (pluginOptions, context) => {
next();

const res = await plugins.publish(context);
pkg._published = true;

debug("published: %s", pkg.name);

Expand All @@ -200,6 +209,7 @@ function createInlinePluginCreator(packages, multiContext, synchronizer, flags)
verifyConditions,
analyzeCommits,
generateNotes,
prepare,
publish,
};

Expand Down
4 changes: 2 additions & 2 deletions lib/getWorkspacesYarn.js
@@ -1,5 +1,5 @@
const glob = require("bash-glob");
const getManifest = require("./getManifest");
const glob = require("./glob");
const { checker } = require("./blork");

/**
Expand All @@ -23,7 +23,7 @@ function getWorkspacesYarn(cwd) {
}

// Turn workspaces into list of package.json files.
const workspaces = glob.sync(
const workspaces = glob(
packages.map((p) => p.replace(/\/?$/, "/package.json")),
{
cwd: cwd,
Expand Down
10 changes: 10 additions & 0 deletions lib/glob.js
@@ -0,0 +1,10 @@
const bashGlob = require("bash-glob");
const bashPath = require("bash-path");

module.exports = (...args) => {
if (!bashPath) {
throw new TypeError("`bash` must be installed"); // TODO move this check to bash-glob
}

return bashGlob.sync(...args);
};
21 changes: 16 additions & 5 deletions lib/multiSemanticRelease.js
@@ -1,5 +1,6 @@
const { dirname } = require("path");
const semanticRelease = require("semantic-release");
const { uniq } = require("lodash");
const { check } = require("./blork");
const getLogger = require("./getLogger");
const getSynchronizer = require("./getSynchronizer");
Expand Down Expand Up @@ -68,7 +69,15 @@ async function multiSemanticRelease(

// Load packages from paths.
const packages = await Promise.all(paths.map((path) => getPackage(path, multiContext)));
packages.forEach((pkg) => logger.success(`Loaded package ${pkg.name}`));
packages.forEach((pkg) => {
// Once we load all the packages we can find their cross refs
// Make a list of local dependencies.
// Map dependency names (e.g. my-awesome-dep) to their actual package objects in the packages array.
pkg.localDeps = uniq(pkg.deps.map((d) => packages.find((p) => d === p.name)).filter(Boolean));

logger.success(`Loaded package ${pkg.name}`);
});

logger.complete(`Queued ${packages.length} packages! Starting release...`);

// Shared signal bus.
Expand All @@ -86,7 +95,7 @@ async function multiSemanticRelease(
await waitFor("_readyForRelease", pkg);
}

return releasePackage(pkg, createInlinePlugin, multiContext);
return releasePackage(pkg, createInlinePlugin, multiContext, flags);
})
);
const released = packages.filter((pkg) => pkg.result).length;
Expand Down Expand Up @@ -150,11 +159,12 @@ async function getPackage(path, { options: globalOptions, env, cwd, stdout, stde
* @param {Package} pkg The specific package.
* @param {Function} createInlinePlugin A function that creates an inline plugin.
* @param {MultiContext} multiContext Context object for the multirelease.
* @param {Object} flags Argv flags.
* @returns {Promise<void>} Promise that resolves when done.
*
* @internal
*/
async function releasePackage(pkg, createInlinePlugin, multiContext) {
async function releasePackage(pkg, createInlinePlugin, multiContext, flags) {
// Vars.
const { options: pkgOptions, name, dir } = pkg;
const { env, stdout, stderr } = multiContext;
Expand All @@ -168,14 +178,15 @@ async function releasePackage(pkg, createInlinePlugin, multiContext) {
// This consists of:
// - The global options (e.g. from the top level package.json)
// - The package options (e.g. from the specific package's package.json)
const options = { ...pkgOptions, ...inlinePlugin };
// TODO filter flags
const options = { ...flags, ...pkgOptions, ...inlinePlugin };

// Add the package name into tagFormat.
// Thought about doing a single release for the tag (merging several packages), but it's impossible to prevent Github releasing while allowing NPM to continue.
// It'd also be difficult to merge all the assets into one release without full editing/overriding the plugins.
options.tagFormat = name + "@${version}";

// This options are needed for plugins that does not rely on `pluginOptions` and extracts them independently.
// This options are needed for plugins that do not rely on `pluginOptions` and extract them independently.
options._pkgOptions = pkgOptions;

// Call semanticRelease() on the directory and save result to pkg.
Expand Down
39 changes: 25 additions & 14 deletions lib/updateDeps.js
Expand Up @@ -39,6 +39,11 @@ const resolveReleaseType = (pkg, bumpStrategy = "override", releaseStrategy = "p
return undefined;
}

// 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.

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

return pkg._nextType;
Expand All @@ -56,40 +61,40 @@ const resolveReleaseType = (pkg, bumpStrategy = "override", releaseStrategy = "p
*/
const getDependentRelease = (pkg, bumpStrategy, releaseStrategy, ignore) => {
const severityOrder = ["patch", "minor", "major"];
const { _localDeps, manifest = {} } = pkg;
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;
return false;
}
const resolvedVersion = resolveNextVersion(currentVersion, nextVersion, releaseStrategy);

const resolvedVersion = resolveNextVersion(currentVersion, nextVersion, releaseStrategy);
if (currentVersion !== resolvedVersion) {
scope[name] = resolvedVersion;

return true;
}

return false;
};

// prettier-ignore
return _localDeps
.filter((p) => ignore.indexOf(p) === -1)
return localDeps
.filter((p) => !ignore.includes(p))
.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 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;
const requireRelease = scopes
.reduce((res, scope) => bumpDependency(scope, name, nextVersion) || res, !lastVersion)

return requireRelease && (severityOrder.indexOf(nextType) > severityOrder.indexOf(releaseType))
? nextType
Expand All @@ -107,10 +112,16 @@ const getDependentRelease = (pkg, bumpStrategy, releaseStrategy, ignore) => {
* @internal
*/
const resolveNextVersion = (currentVersion, nextVersion, strategy = "override") => {
if (strategy === "satisfy" && semver.satisfies(nextVersion, 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.
if ((strategy === "satisfy" || strategy === "inherit") && semver.satisfies(nextVersion, currentVersion)) {
return currentVersion;
}

// `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.
if (strategy === "inherit") {
const sep = ".";
const nextChunks = nextVersion.split(sep);
Expand All @@ -125,15 +136,15 @@ const resolveNextVersion = (currentVersion, nextVersion, strategy = "override")
return resolvedChunks.join(sep);
}

// By default next package version would be set as is for the all dependants
// "override"
// 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
*/
Expand All @@ -145,7 +156,7 @@ const updateManifestDeps = (pkg) => {
manifest.version = getManifest(path).version;

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

Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -64,7 +64,7 @@
"lodash": "^4.17.20",
"meow": "^8.0.0",
"promise-events": "^0.2.1",
"semantic-release": "^17.2.4",
"semantic-release": "^17.3.0",
"semver": "^7.3.2",
"signale": "^1.4.0",
"stream-buffers": "^3.0.2",
Expand Down
11 changes: 11 additions & 0 deletions test/lib/getWorkspacesYarn.test.js
Expand Up @@ -36,4 +36,15 @@ describe("getWorkspacesYarn()", () => {
`${resolved}/packages/d/package.json`,
]);
});
test("Checks `bash` to be installed", () => {
jest.isolateModules(() => {
jest.resetModules();
jest.mock("bash-path", () => undefined);

const resolved = resolve(`${__dirname}/../fixtures/yarnWorkspaces`);
const getWorkspaces = require("../../lib/getWorkspacesYarn");

expect(() => getWorkspaces(resolved)).toThrowError("`bash` must be installed");
});
});
});
3 changes: 2 additions & 1 deletion test/lib/multiSemanticRelease.test.js
Expand Up @@ -225,7 +225,8 @@ describe("multiSemanticRelease()", () => {
`packages/a/package.json`,
],
{},
{ cwd, stdout, stderr }
{ cwd, stdout, stderr },
{ deps: {}, dryRun: false }
);

// Get stdout and stderr output.
Expand Down

0 comments on commit a7aa625

Please sign in to comment.