Skip to content

Commit

Permalink
Add linked option/lockstep (#27)
Browse files Browse the repository at this point in the history
* Add linked option to config

* Remove a thing

* Add docs

* Add a changeset
  • Loading branch information
emmatown authored and Noviny committed May 7, 2019
1 parent ce0a788 commit 6929624
Show file tree
Hide file tree
Showing 14 changed files with 295 additions and 20 deletions.
4 changes: 4 additions & 0 deletions .changeset/a198bff6/changes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"releases": [{ "name": "@changesets/cli", "type": "minor" }],
"dependents": []
}
1 change: 1 addition & 0 deletions .changeset/a198bff6/changes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add linked packages/lockstep
79 changes: 79 additions & 0 deletions docs/linked-packages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Linked Packages

Linked packages allow you to specify a group or groups of packages that should be versioned together. There are some complex cases, so some examples are shown below to demonstrate various cases.

- Linked packages will still only bumped when there is a changeset for them (this can mean because you explicitly choose to add a changeset for it or because it's a dependant of something being released)
- Packages that have changesets and are in a set of linked packages will **always** be versioned to the highest current version in the set of linked packages + the highest bump type from changesets in the set of linked packages

## Examples

### General example

I have three packages, `pkg-a`, `pkg-b` and `pkg-c`. `pkg-a` and `pkg-b` are linked but `pkg-c` is not so the config looks like this.

```jsx
// stuff...

const versionOptions = {
...otherStuff,
linked: [["pkg-a", "pkg-b"]]
};

// stuff...
```

- `pkg-a` is at `1.0.0`
- `pkg-b` is at `1.0.0`
- `pkg-c` is at `1.0.0`

I have a changeset with a patch for `pkg-a`, minor for `pkg-b` and major for `pkg-c` and I do a release, the resulting versions will be:

- `pkg-a` is at `1.1.0`
- `pkg-b` is at `1.1.0`
- `pkg-c` is at `2.0.0`

I now have another changeset with a minor for `pkg-a` and I do a release, the resulting versions will be:

- `pkg-a` is at `1.2.0`
- `pkg-b` is at `1.1.0`
- `pkg-c` is at `2.0.0`

I now have another changeset with a minor for `pkg-b` and I do a release, the resulting versions will be:

- `pkg-a` is at `1.2.0`
- `pkg-b` is at `1.3.0`
- `pkg-c` is at `2.0.0`

I now have another changeset with patches for all three packages and I do a release, the resulting versions will be:

- `pkg-a` is at `1.3.1`
- `pkg-b` is at `1.3.1`
- `pkg-c` is at `2.0.1`

### Example with dependants

I have two packages, `pkg-a`, `pkg-b` which are linked. `pkg-a` has a dependency on `pkg-b`.

```jsx
// stuff...

const versionOptions = {
...otherStuff,
linked: [["pkg-a", "pkg-b"]]
};

// stuff...
```

- `pkg-a` is at `1.0.0`
- `pkg-b` is at `1.0.0`

I have a changeset with a major for `pkg-b` and I do a release, the resulting versions will be:

- `pkg-a` is at `2.0.0`
- `pkg-b` is at `2.0.0`

I now have another changeset with a major for `pkg-a` and I do a release, the resulting versions will be:

- `pkg-a` is at `3.0.0`
- `pkg-b` is at `2.0.0`
6 changes: 5 additions & 1 deletion packages/cli/default-files/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ const versionOptions = {
// A function that returns a string. It takes in options about a change. This allows you to customise your changelog entries
getReleaseLine,
// A function that returns a string. It takes in options about when a pacakge is updated because
getDependencyReleaseLine
getDependencyReleaseLine,
// An array of arrays that defines packages that are linked.
// Linked packages are packages that should be at the same version when they're released.
// If you've used Lerna to version packages before, this is very similar.
linked: [[]]
};

const publishOptions = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# We just want a file in here so git collects it
88 changes: 88 additions & 0 deletions packages/cli/src/__fixtures__/linked-packages/.changeset/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
Hey, welcome to the changeset config! This file has been generated
for you with the default configs we use, and some comments around
what options mean, so that it's easy to customise your workflow.
You should update this as you need to craft your workflow.
Config provided by a CI command takes precedence over the contents of this file.
If a config option isn't present here, we will fall back to the defaults.
*/

const changesetOptions = {
// If true, we will automatically commit the changeset when the command is run
commit: false
};

// This function takes information about a changeset to generate an entry for it in your
// changelog. We provide the full changeset object as well as the version.
// It may be a good idea to replace the commit hash with a link to the commit.

/* the default shape is:
### Bump Type
- GIT_HASH: A summary message you wrote, indented?
*/

// eslint-disable-next-line no-unused-vars
const getReleaseLine = async (changeset, type) => {
const [firstLine, ...futureLines] = changeset.summary
.split("\n")
.map(l => l.trimRight());

return `- ${changeset.commit}: ${firstLine}\n${futureLines
.map(l => ` ${l}`)
.join("\n")}`;
};

// This function takes information about what dependencies we are updating in the package.
// It provides an array of related changesets, as well as the dependencies updated.

/*
- Updated dependencies: [ABCDEFG]:
- Updated dependencies: [HIJKLMN]:
- dependencyA@1.0.1
- dependencyb@1.2.0
*/
const getDependencyReleaseLine = async (changesets, dependenciesUpdated) => {
if (dependenciesUpdated.length === 0) return "";

const changesetLinks = changesets.map(
changeset => `- Updated dependencies [${changeset.commit}]:`
);

const updatedDepenenciesList = dependenciesUpdated.map(
dependency => ` - ${dependency.name}@${dependency.version}`
);

return [...changesetLinks, ...updatedDepenenciesList].join("\n");
};

const versionOptions = {
// If true, we will automatically commit the version updating when the command is run
commit: false,
// Adds a skipCI flag to the commit - only valid if `commit` is also true.
skipCI: false,
// Do not modify the `changelog.md` files for packages that are updated
updateChangelog: true,
// A function that returns a string. It takes in options about a change. This allows you to customise your changelog entries
getReleaseLine,
// A function that returns a string. It takes in options about when a pacakge is updated because
getDependencyReleaseLine,
// An array of arrays that defines packages that are linked.
// Linked packages are packages that should be at the same version when they're released.
// If you've used Lerna to version packages before, this is very similar.
linked: [["pkg-a", "pkg-b"]]
};

const publishOptions = {
// This sets whether unpublished packages are public by default. We err on the side of caution here.
public: false
};

module.exports = {
versionOptions,
changesetOptions,
publishOptions
};
11 changes: 11 additions & 0 deletions packages/cli/src/__fixtures__/linked-packages/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"private": true,
"name": "simple-project",
"description": "three projects, each depending on one other",
"version": "1.0.0",
"bolt": {
"workspaces": [
"packages/*"
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "pkg-a",
"version": "1.0.0",
"dependencies": {
"pkg-b": "0.1.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "pkg-b",
"version": "0.1.0"
}
16 changes: 16 additions & 0 deletions packages/cli/src/commands/bump/__tests__/consume.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,22 @@ describe("running version in a simple project", () => {
expect(git.commit).toHaveBeenCalledTimes(1);
});

it("should bump packages to the correct versions when packages are linked", async () => {
const cwd2 = await copyFixtureIntoTempDir(__dirname, "linked-packages");
const spy = jest.spyOn(fs, "writeFile");
await writeChangesets([simpleChangeset2], cwd2);

await versionCommand({ cwd: cwd2 });
const calls = spy.mock.calls;

expect(JSON.parse(calls[0][1])).toEqual(
expect.objectContaining({ name: "pkg-a", version: "1.1.0" })
);
expect(JSON.parse(calls[1][1])).toEqual(
expect.objectContaining({ name: "pkg-b", version: "1.1.0" })
);
});

describe("when there are multiple changeset commits", () => {
it("should bump releasedPackages", async () => {
await writeChangesets([simpleChangeset, simpleChangeset2], cwd);
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/commands/bump/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ export default async function version(opts) {
removeEmptyFolders(changesetBase);

const unreleasedChangesets = await getChangesets(changesetBase);
const releaseObj = createRelease(unreleasedChangesets, allPackages);
const releaseObj = createRelease(
unreleasedChangesets,
allPackages,
config.linked
);
const publishCommit = createReleaseCommit(releaseObj, config.skipCI);

if (unreleasedChangesets.length === 0) {
Expand Down
23 changes: 21 additions & 2 deletions packages/cli/src/commands/status/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,27 @@ import getChangesets from "../../utils/getChangesets";
import * as bolt from "../../utils/bolt-replacements";

import createRelease from "../../utils/createRelease";
import { defaultConfig } from "../../utils/constants";
import resolveConfig from "../../utils/resolveConfig";

export default async function getStatus({
cwd,
sinceMaster,
verbose,
output,
...opts
}) {
let userConfig = await resolveConfig({
cwd,
sinceMaster,
verbose,
output,
...opts
});
userConfig =
userConfig && userConfig.versionOptions ? userConfig.versionOptions : {};
const config = { ...defaultConfig.versionOptions, ...userConfig, ...opts };

export default async function getStatus({ cwd, sinceMaster, verbose, output }) {
const changesetBase = await getChangesetBase(cwd);
const allPackages = await bolt.getWorkspaces({ cwd });
// TODO: Check if we are no master and give a different error message if we are
Expand All @@ -23,7 +42,7 @@ export default async function getStatus({ cwd, sinceMaster, verbose, output }) {
process.exit(1);
}

const releaseObject = createRelease(changesets, allPackages);
const releaseObject = createRelease(changesets, allPackages, config.linked);
const { releases } = releaseObject;
logger.log("---");

Expand Down
34 changes: 27 additions & 7 deletions packages/cli/src/utils/createRelease/flattenChangesets.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ function maxType(types) {
return "none";
}

export default function flattenReleases(changesets) {
export default function flattenReleases(changesets, allLinkedPackages) {
const flatChangesets = changesets
.map(changeset => [
...changeset.releases.map(release => ({
Expand All @@ -30,10 +30,30 @@ export default function flattenReleases(changesets) {
return acc;
}, {});

return Object.entries(flatChangesets).map(([name, releases]) => ({
name,
type: maxType(releases.map(r => r.type)),
commits: [...new Set(releases.map(r => r.commit))].filter(a => a),
changesets: [...new Set(releases.map(r => r.id))]
}));
const flatReleases = new Map(
Object.entries(flatChangesets).map(([name, releases]) => [
name,
{
name,
type: maxType(releases.map(r => r.type)),
commits: [...new Set(releases.map(r => r.commit))].filter(a => a),
changesets: [...new Set(releases.map(r => r.id))]
}
])
);

for (const linkedPackages of allLinkedPackages) {
const allBumpTypes = [];
for (let linkedPackage of linkedPackages) {
let release = flatReleases.get(linkedPackage);
allBumpTypes.push(release.type);
}
const highestBumpType = maxType(allBumpTypes);
for (let linkedPackage of linkedPackages) {
let release = flatReleases.get(linkedPackage);
release.type = highestBumpType;
}
}

return [...flatReleases.values()];
}
35 changes: 26 additions & 9 deletions packages/cli/src/utils/createRelease/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,41 @@ import flattenChangesets from "./flattenChangesets";
}
*/

function getCurrentVersion(packageName, allPackages) {
const pkg = allPackages.find(p => p.name === packageName);
// When changeset contains deleted package returning null as its version
return pkg ? pkg.config.version : null;
}

export default function createRelease(changesets, allPackages) {
export default function createRelease(
changesets,
allPackages,
allLinkedPackages = []
) {
// First, combine all the changeset.releases into one useful array

const flattenedChangesets = flattenChangesets(changesets);
const flattenedChangesets = flattenChangesets(changesets, allLinkedPackages);

let currentVersions = new Map();

for (let pkg of allPackages) {
currentVersions.set(pkg.name, pkg ? pkg.config.version : null);
}

for (let linkedPackages of allLinkedPackages) {
let highestVersion;
for (let linkedPackage of linkedPackages) {
let version = currentVersions.get(linkedPackage);
if (highestVersion === undefined || semver.gt(version, highestVersion)) {
highestVersion = version;
}
}
for (let linkedPackage of linkedPackages) {
currentVersions.set(linkedPackage, highestVersion);
}
}

const allReleases = flattenedChangesets
// do not update none packages
.filter(release => release.type !== "none")
// get the current version for each package
.map(release => ({
...release,
version: getCurrentVersion(release.name, allPackages)
version: currentVersions.get(release.name)
}))
// update to new version for each package
.map(release => ({
Expand Down

0 comments on commit 6929624

Please sign in to comment.