Repository: atlassian/changesets (ts)

Issue:
Fix messaging for single-package changelog generation
Currently, you get asked 'which of these packages is major/minor?' and select it

Single package should have slightly modified workflow to be 'should this package be bumped as a major/minor' as a boolean option.

Thanks [💯💜🎉](https://github.com/Noviny/emoji-uid) for revealing this problem.

Pull request:
Use a single prompt for the semver version type when there is only a single package
Closes #98 

![changeset add command with single package repo](https://user-images.githubusercontent.com/11481355/75123292-40f78f00-56fa-11ea-9e16-fe3f5e2d9cf5.gif)


Patch:
diff --git a/.changeset/gold-dogs-cross.md b/.changeset/gold-dogs-cross.md
new file mode 100644
index 0000000..7d0e08d
--- /dev/null
+++ b/.changeset/gold-dogs-cross.md
@@ -0,0 +1,7 @@
+---
+"@changesets/cli": patch
+---
+
+Use a single prompt for the semver version type when there is only a single package in the repo in `changeset add`
+
+![changeset add command with single package repo](https://user-images.githubusercontent.com/11481355/75123292-40f78f00-56fa-11ea-9e16-fe3f5e2d9cf5.gif)
diff --git a/__fixtures__/single-package/.changeset/config.json b/__fixtures__/single-package/.changeset/config.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/__fixtures__/single-package/.changeset/config.json
@@ -0,0 +1,1 @@
+{}
diff --git a/__fixtures__/single-package/package.json b/__fixtures__/single-package/package.json
new file mode 100644
index 0000000..ab0e06d
--- /dev/null
+++ b/__fixtures__/single-package/package.json
@@ -0,0 +1,6 @@
+{
+  "private": true,
+  "name": "single-package",
+  "description": "Nothing but the root project, a non-mono-repo",
+  "version": "1.0.0"
+}
diff --git a/packages/cli/src/commands/add/createChangeset.ts b/packages/cli/src/commands/add/createChangeset.ts
index 06b6308..9238c4a 100644
--- a/packages/cli/src/commands/add/createChangeset.ts
+++ b/packages/cli/src/commands/add/createChangeset.ts
@@ -3,12 +3,36 @@ import chalk from "chalk";
 import semver from "semver";
 
 import * as cli from "../../utils/cli-utilities";
-import getWorkspaces from "../../utils/getWorkspaces";
 import { error, log } from "@changesets/logger";
-import { Release, Workspace } from "@changesets/types";
+import { Release, Workspace, PackageJSON } from "@changesets/types";
+import { ExitError } from "@changesets/errors";
 
 const { green, yellow, red, bold, blue, cyan } = chalk;
 
+async function confirmMajorRelease(pkgJSON: PackageJSON) {
+  if (semver.lt(pkgJSON.version, "1.0.0")) {
+    // prettier-ignore
+    log(yellow(`WARNING: Releasing a major version for ${green(pkgJSON.name)} will be its ${red('first major release')}.`))
+    log(
+      yellow(
+        `If you are unsure if this is correct, contact the package's maintainers$ ${red(
+          "before committing this changeset"
+        )}.`
+      )
+    );
+
+    let shouldReleaseFirstMajor = await cli.askConfirm(
+      bold(
+        `Are you sure you want still want to release the ${red(
+          "first major release"
+        )} of ${pkgJSON.name}?`
+      )
+    );
+    return shouldReleaseFirstMajor;
+  }
+  return true;
+}
+
 async function getPackagesToRelease(
   changedPackages: Array<string>,
   allPackages: Array<Workspace>
@@ -73,71 +97,25 @@ function formatPkgNameAndVersion(pkgName: string, version: string) {
 
 export default async function createChangeset(
   changedPackages: Array<string>,
-  cwd: string
+  allPackages: Workspace[]
 ): Promise<{ summary: string; releases: Array<Release> }> {
-  const allPackages = await getWorkspaces({ cwd });
-  const packagesToRelease = await getPackagesToRelease(
-    changedPackages,
-    allPackages
-  );
-
-  let pkgJsonsByName = new Map(
-    allPackages.map(({ name, config }) => [name, config])
-  );
-
   const releases: Array<Release> = [];
 
-  let pkgsLeftToGetBumpTypeFor = new Set(packagesToRelease);
-
-  let pkgsThatShouldBeMajorBumped = await cli.askCheckboxPlus(
-    bold(`Which packages should have a ${red("major")} bump?`),
-    packagesToRelease.map(pkgName => {
-      return {
-        name: pkgName,
-        message: formatPkgNameAndVersion(
-          pkgName,
-          pkgJsonsByName.get(pkgName)!.version
-        )
-      };
-    })
-  );
-
-  for (const pkgName of pkgsThatShouldBeMajorBumped) {
-    // for packages that are under v1, we want to make sure major releases are intended,
-    // as some repo-wide sweeping changes have mistakenly release first majors
-    // of packages.
-    let { version } = pkgJsonsByName.get(pkgName)!;
-    if (semver.lt(version, "1.0.0")) {
-      // prettier-ignore
-      log(yellow(`WARNING: Releasing a major version for ${green(pkgName)} will be its ${red('first major release')}.`))
-      log(
-        yellow(
-          `If you are unsure if this is correct, contact the package's maintainers$ ${red(
-            "before committing this changeset"
-          )}.`
-        )
-      );
+  if (allPackages.length > 1) {
+    const packagesToRelease = await getPackagesToRelease(
+      changedPackages,
+      allPackages
+    );
 
-      let shouldReleaseFirstMajor = await cli.askConfirm(
-        bold(
-          `Are you sure you want still want to release the ${red(
-            "first major release"
-          )} of ${pkgName}?`
-        )
-      );
-      if (!shouldReleaseFirstMajor) {
-        continue;
-      }
-    }
-    pkgsLeftToGetBumpTypeFor.delete(pkgName);
+    let pkgJsonsByName = new Map(
+      allPackages.map(({ name, config }) => [name, config])
+    );
 
-    releases.push({ name: pkgName, type: "major" });
-  }
+    let pkgsLeftToGetBumpTypeFor = new Set(packagesToRelease);
 
-  if (pkgsLeftToGetBumpTypeFor.size !== 0) {
-    let pkgsThatShouldBeMinorBumped = await cli.askCheckboxPlus(
-      bold(`Which packages should have a ${green("minor")} bump?`),
-      [...pkgsLeftToGetBumpTypeFor].map(pkgName => {
+    let pkgsThatShouldBeMajorBumped = await cli.askCheckboxPlus(
+      bold(`Which packages should have a ${red("major")} bump?`),
+      packagesToRelease.map(pkgName => {
         return {
           name: pkgName,
           message: formatPkgNameAndVersion(
@@ -148,24 +126,69 @@ export default async function createChangeset(
       })
     );
 
-    for (const pkgName of pkgsThatShouldBeMinorBumped) {
-      pkgsLeftToGetBumpTypeFor.delete(pkgName);
+    for (const pkgName of pkgsThatShouldBeMajorBumped) {
+      // for packages that are under v1, we want to make sure major releases are intended,
+      // as some repo-wide sweeping changes have mistakenly release first majors
+      // of packages.
+      let pkgJson = pkgJsonsByName.get(pkgName)!;
 
-      releases.push({ name: pkgName, type: "minor" });
+      let shouldReleaseFirstMajor = await confirmMajorRelease(pkgJson);
+      if (shouldReleaseFirstMajor) {
+        pkgsLeftToGetBumpTypeFor.delete(pkgName);
+
+        releases.push({ name: pkgName, type: "major" });
+      }
     }
-  }
 
-  if (pkgsLeftToGetBumpTypeFor.size !== 0) {
-    log(`The following packages will be ${blue("patch")} bumped:`);
-    pkgsLeftToGetBumpTypeFor.forEach(pkgName => {
-      log(
-        formatPkgNameAndVersion(pkgName, pkgJsonsByName.get(pkgName)!.version)
+    if (pkgsLeftToGetBumpTypeFor.size !== 0) {
+      let pkgsThatShouldBeMinorBumped = await cli.askCheckboxPlus(
+        bold(`Which packages should have a ${green("minor")} bump?`),
+        [...pkgsLeftToGetBumpTypeFor].map(pkgName => {
+          return {
+            name: pkgName,
+            message: formatPkgNameAndVersion(
+              pkgName,
+              pkgJsonsByName.get(pkgName)!.version
+            )
+          };
+        })
       );
-    });
 
-    for (const pkgName of pkgsLeftToGetBumpTypeFor) {
-      releases.push({ name: pkgName, type: "patch" });
+      for (const pkgName of pkgsThatShouldBeMinorBumped) {
+        pkgsLeftToGetBumpTypeFor.delete(pkgName);
+
+        releases.push({ name: pkgName, type: "minor" });
+      }
+    }
+
+    if (pkgsLeftToGetBumpTypeFor.size !== 0) {
+      log(`The following packages will be ${blue("patch")} bumped:`);
+      pkgsLeftToGetBumpTypeFor.forEach(pkgName => {
+        log(
+          formatPkgNameAndVersion(pkgName, pkgJsonsByName.get(pkgName)!.version)
+        );
+      });
+
+      for (const pkgName of pkgsLeftToGetBumpTypeFor) {
+        releases.push({ name: pkgName, type: "patch" });
+      }
+    }
+  } else {
+    let pkg = allPackages[0];
+    let type = await cli.askList(
+      `What kind of change is this for ${green(
+        pkg.name
+      )}? (current version is ${pkg.config.version})`,
+      ["patch", "minor", "major"]
+    );
+    console.log(type);
+    if (type === "major") {
+      let shouldReleaseAsMajor = await confirmMajorRelease(pkg.config);
+      if (!shouldReleaseAsMajor) {
+        throw new ExitError(1);
+      }
     }
+    releases.push({ name: pkg.name, type });
   }
 
   log(
diff --git a/packages/cli/src/commands/add/index.ts b/packages/cli/src/commands/add/index.ts
index 365cf8e..f38589b 100644
--- a/packages/cli/src/commands/add/index.ts
+++ b/packages/cli/src/commands/add/index.ts
@@ -9,12 +9,14 @@ import { Config } from "@changesets/types";
 import writeChangeset from "./writeChangeset";
 import createChangeset from "./createChangeset";
 import printConfirmationMessage from "./messages";
+import getWorkspaces from "../../utils/getWorkspaces";
 
 export default async function add(
   cwd: string,
   { empty }: { empty?: boolean },
   config: Config
 ) {
+  const allPackages = await getWorkspaces({ cwd });
   const changesetBase = path.resolve(cwd, ".changeset");
 
   let newChangeset, confirmChangeset;
@@ -32,8 +34,8 @@ export default async function add(
     const changePackagesName = changedPackages
       .filter(a => a)
       .map(pkg => pkg.name);
-    newChangeset = await createChangeset(changePackagesName, cwd);
-    printConfirmationMessage(newChangeset);
+    newChangeset = await createChangeset(changePackagesName, allPackages);
+    printConfirmationMessage(newChangeset, allPackages.length > 1);
 
     confirmChangeset = await cli.askConfirm("Is this your desired changeset?");
   }
diff --git a/packages/cli/src/commands/add/messages.ts b/packages/cli/src/commands/add/messages.ts
index ded4aba..dbcabc0 100644
--- a/packages/cli/src/commands/add/messages.ts
+++ b/packages/cli/src/commands/add/messages.ts
@@ -4,10 +4,13 @@ import outdent from "outdent";
 import { log } from "@changesets/logger";
 import { Release, VersionType } from "@changesets/types";
 
-export default function printConfirmationMessage(changeset: {
-  releases: Array<Release>;
-  summary: string;
-}) {
+export default function printConfirmationMessage(
+  changeset: {
+    releases: Array<Release>;
+    summary: string;
+  },
+  repoHasMultiplePackages: boolean
+) {
   function getReleasesOfType(type: VersionType) {
     return changeset.releases
       .filter(release => release.type === type)
@@ -24,15 +27,16 @@ export default function printConfirmationMessage(changeset: {
     log(`${chalk.green("[Minor]")}\n  ${minorReleases.join(", ")}`);
   if (patchReleases.length > 0)
     log(`${chalk.green("[Patch]")}\n  ${patchReleases.join(", ")}`);
-
-  const message = outdent`
+  if (repoHasMultiplePackages) {
+    const message = outdent`
       ${chalk.red("========= NOTE ========")}
       All dependents of these packages that will be incompatible with the new version will be ${chalk.red(
         "patch bumped"
       )} when this changeset is applied.`;
-  const prettyMessage = boxen(message, {
-    borderStyle: "double",
-    align: "center"
-  });
-  log(prettyMessage);
+    const prettyMessage = boxen(message, {
+      borderStyle: "double",
+      align: "center"
+    });
+    log(prettyMessage);
+  }
 }
diff --git a/packages/cli/src/utils/cli-utilities.ts b/packages/cli/src/utils/cli-utilities.ts
index 9734acd..c290bdb 100644
--- a/packages/cli/src/utils/cli-utilities.ts
+++ b/packages/cli/src/utils/cli-utilities.ts
@@ -85,10 +85,10 @@ async function askConfirm(message: string): Promise<boolean> {
     });
 }
 
-async function askList(
+async function askList<Choice extends string>(
   message: string,
-  choices: Array<string>
-): Promise<string> {
+  choices: readonly Choice[]
+): Promise<Choice> {
   const name = `List-${serialId()}`;
 
   return prompt([


Test patch:
diff --git a/packages/cli/src/commands/add/__tests__/add.ts b/packages/cli/src/commands/add/__tests__/add.ts
index 90759fa..96c9372 100644
--- a/packages/cli/src/commands/add/__tests__/add.ts
+++ b/packages/cli/src/commands/add/__tests__/add.ts
@@ -7,7 +7,8 @@ import { temporarilySilenceLogs } from "@changesets/test-utils";
 import {
   askCheckboxPlus,
   askConfirm,
-  askQuestion
+  askQuestion,
+  askList
 } from "../../../utils/cli-utilities";
 import addChangeset from "..";
 import writeChangeset from "../writeChangeset";
@@ -86,6 +87,40 @@ describe("Changesets", () => {
     const call = writeChangeset.mock.calls[0][0];
     expect(call).toEqual(expectedChangeset);
   });
+  it("should generate a changeset in a single package repo", async () => {
+    const cwd = await copyFixtureIntoTempDir(__dirname, "single-package");
+
+    const summary = "summary message mock";
+
+    // @ts-ignore
+    askList.mockReturnValueOnce(Promise.resolve("minor"));
+
+    let confirmAnswers = {
+      "Is this your desired changeset?": true
+    };
+    // @ts-ignore
+    askQuestion.mockReturnValueOnce(summary);
+    // @ts-ignore
+    askConfirm.mockImplementation(question => {
+      question = stripAnsi(question);
+      // @ts-ignore
+      if (confirmAnswers[question]) {
+        // @ts-ignore
+        return confirmAnswers[question];
+      }
+      throw new Error(`An answer could not be found for ${question}`);
+    });
+
+    await addChangeset(cwd, { empty: false }, defaultConfig);
+
+    const expectedChangeset = {
+      summary: "summary message mock",
+      releases: [{ name: "single-package", type: "minor" }]
+    };
+    // @ts-ignore
+    const call = writeChangeset.mock.calls[0][0];
+    expect(call).toEqual(expectedChangeset);
+  });
   it("should commit when the commit flag is passed in", async () => {
     const cwd = await copyFixtureIntoTempDir(
       __dirname,