Repository: typescript-eslint/tslint-to-eslint-config (ts)

Issue:
Don't request installing packages already present in package.json
### 🚀 Feature Request

Right now, `tslint-to-eslint-config` will report any packages that must be required for the generated ESLint configuration:

We already read from `package.json` and support a `--package` flag. We could save a little bit of CLI output space skipping printing packages that are already listed in `package.json` as a `dependency` or `devDependency`.

#### Existing Behavior

```
⚡ 7 packages are required for this ESLint configuration. ⚡
  npm install @typescript-eslint/eslint-plugin @typescript-eslint/parser @typescript-eslint/eslint-plugin-tslint eslint eslint-plugin-import eslint-plugin-jsdoc eslint-plugin-prefer-arrow --save-dev
```

#### Change Proposal

If all those packages already exist, the :zap: part of CLI output could be skipped.

If only some already exist, we could skip printing them.


```
⚡ 3 new packages are required for this ESLint configuration. ⚡
  npm install @typescript-eslint/eslint-plugin-tslint eslint-plugin-jsdoc eslint-plugin-prefer-arrow --save-dev
```


Pull request:
Removed already-installed packages from install command
<!--
👋 Hi, thanks for sending a PR to tslint-to-eslint-config! 💖
Please fill out all fields below to ensure your PR is reviewed quickly.
-->

## PR Checklist

-   [x] Addresses an existing issue: fixes #416
-   [x] That issue was marked as [`status: accepting prs`](https://github.com/typescript-eslint/tslint-to-eslint-config/labels/status%3A%20accepting%20prs)

## Overview

Separates `logMissingPackages` out from the multi-purpose `reportConversionResults` and directly calls it in `convertConfig`. It'll now take in the result of reading `--package` (normally `package.json`) and remove any package names contained in `dependencies` or `devDependencies` from the list of suggested packages to install.

![Screenshot of the trim new CLI output](https://user-images.githubusercontent.com/3335181/80329759-487a0800-8811-11ea-8ccf-64cf74c029f6.png)

Per the notes in #416, we can't look at the `extends` value of an ESLint configuration file for packages that need to be installed - even though `eslint-config-prettier` isn't added to `plugins`. This PR adds in a special case for `eslint-config-prettier` existing in an `extends`. I don't know of any other ones we'll need to add to that special case... for now.

Also adds `plugin:` before plugin names, because that seems to be recommended by the ESLint docs. 

Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index c98de62..981a771 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -38,6 +38,7 @@ module.exports = {
         "@typescript-eslint/no-unsafe-return": "off",
         "@typescript-eslint/no-untyped-public-signature": "off",
         "@typescript-eslint/no-unused-vars": "off",
+        "@typescript-eslint/no-unused-vars-experimental": "off",
         "@typescript-eslint/no-use-before-define": "off",
         "@typescript-eslint/prefer-readonly-parameter-types": "off",
         "@typescript-eslint/prefer-reduce-type-parameter": "off",
diff --git a/src/cli/main.ts b/src/cli/main.ts
index bc28831..a55117c 100644
--- a/src/cli/main.ts
+++ b/src/cli/main.ts
@@ -44,7 +44,14 @@ import { findTSLintConfiguration } from "../input/findTSLintConfiguration";
 import { findTypeScriptConfiguration } from "../input/findTypeScriptConfiguration";
 import { importer, ImporterDependencies } from "../input/importer";
 import { mergeLintConfigurations } from "../input/mergeLintConfigurations";
-import { choosePackageManager } from "../reporting/packages/choosePackageManager";
+import {
+    ChoosePackageManagerDependencies,
+    choosePackageManager,
+} from "../reporting/packages/choosePackageManager";
+import {
+    logMissingPackages,
+    LogMissingPackagesDependencies,
+} from "../reporting/packages/logMissingPackages";
 import {
     reportConversionResults,
     ReportConversionResultsDependencies,
@@ -90,15 +97,19 @@ const findOriginalConfigurationsDependencies: FindOriginalConfigurationsDependen
     mergeLintConfigurations,
 };
 
-const choosePackageManagerDependencies = {
+const choosePackageManagerDependencies: ChoosePackageManagerDependencies = {
     fileSystem: fsFileSystem,
 };
 
-const reportConversionResultsDependencies: ReportConversionResultsDependencies = {
+const logMissingPackagesDependencies: LogMissingPackagesDependencies = {
     choosePackageManager: bind(choosePackageManager, choosePackageManagerDependencies),
     logger: processLogger,
 };
 
+const reportConversionResultsDependencies: ReportConversionResultsDependencies = {
+    logger: processLogger,
+};
+
 const retrieveExtendsValuesDependencies: RetrieveExtendsValuesDependencies = {
     importer: boundImporter,
 };
@@ -132,6 +143,7 @@ const convertConfigDependencies: ConvertConfigDependencies = {
         findOriginalConfigurations,
         findOriginalConfigurationsDependencies,
     ),
+    logMissingPackages: bind(logMissingPackages, logMissingPackagesDependencies),
     reportConversionResults: bind(reportConversionResults, reportConversionResultsDependencies),
     simplifyPackageRules: bind(simplifyPackageRules, simplifyPackageRulesDependencies),
     writeConversionResults: bind(writeConversionResults, writeConversionResultsDependencies),
diff --git a/src/conversion/convertConfig.ts b/src/conversion/convertConfig.ts
index d7bbae2..4d96531 100644
--- a/src/conversion/convertConfig.ts
+++ b/src/conversion/convertConfig.ts
@@ -2,6 +2,7 @@ import { SansDependencies } from "../binding";
 import { simplifyPackageRules } from "../creation/simplification/simplifyPackageRules";
 import { writeConversionResults } from "../creation/writeConversionResults";
 import { findOriginalConfigurations } from "../input/findOriginalConfigurations";
+import { logMissingPackages } from "../reporting/packages/logMissingPackages";
 import { reportConversionResults } from "../reporting/reportConversionResults";
 import { convertRules } from "../rules/convertRules";
 import { ResultStatus, ResultWithStatus, TSLintToESLintSettings } from "../types";
@@ -9,6 +10,7 @@ import { ResultStatus, ResultWithStatus, TSLintToESLintSettings } from "../types
 export type ConvertConfigDependencies = {
     convertRules: SansDependencies<typeof convertRules>;
     findOriginalConfigurations: SansDependencies<typeof findOriginalConfigurations>;
+    logMissingPackages: SansDependencies<typeof logMissingPackages>;
     reportConversionResults: SansDependencies<typeof reportConversionResults>;
     simplifyPackageRules: SansDependencies<typeof simplifyPackageRules>;
     writeConversionResults: SansDependencies<typeof writeConversionResults>;
@@ -56,6 +58,10 @@ export const convertConfig = async (
 
     // 5. A summary of the results is printed to the user's console
     await dependencies.reportConversionResults(settings.config, simplifiedConfiguration);
+    await dependencies.logMissingPackages(
+        simplifiedConfiguration,
+        originalConfigurations.data.packages,
+    );
 
     return {
         status: ResultStatus.Succeeded,
diff --git a/src/creation/simplification/simplifyPackageRules.ts b/src/creation/simplification/simplifyPackageRules.ts
index e16f18b..3de99f5 100644
--- a/src/creation/simplification/simplifyPackageRules.ts
+++ b/src/creation/simplification/simplifyPackageRules.ts
@@ -36,7 +36,10 @@ export const simplifyPackageRules = async (
 
     // 3a. If no output rules conflict with `eslint-config-prettier`, it's added in
     if (await dependencies.addPrettierExtensions(ruleConversionResults, prettierRequested)) {
-        allExtensions.push("eslint-config-prettier", "eslint-config-prettier/@typescript-eslint");
+        allExtensions.push(
+            "plugin:eslint-config-prettier",
+            "plugin:eslint-config-prettier/@typescript-eslint",
+        );
     }
 
     if (allExtensions.length === 0) {
diff --git a/src/input/findPackagesConfiguration.ts b/src/input/findPackagesConfiguration.ts
index 213f657..42c79b1 100644
--- a/src/input/findPackagesConfiguration.ts
+++ b/src/input/findPackagesConfiguration.ts
@@ -4,17 +4,8 @@ import {
 } from "./findReportedConfiguration";
 
 export type PackagesConfiguration = {
-    dependencies: {
-        [i: string]: string;
-    };
-    devDependencies: {
-        [i: string]: string;
-    };
-};
-
-const defaultPackagesConfiguration = {
-    dependencies: {},
-    devDependencies: {},
+    dependencies: Record<string, string>;
+    devDependencies: Record<string, string>;
 };
 
 export const findPackagesConfiguration = async (
@@ -32,11 +23,9 @@ export const findPackagesConfiguration = async (
         : {
               dependencies: {
                   ...rawConfiguration.dependencies,
-                  ...defaultPackagesConfiguration.dependencies,
               },
               devDependencies: {
                   ...rawConfiguration.devDependencies,
-                  ...defaultPackagesConfiguration.devDependencies,
               },
           };
 };
diff --git a/src/reporting/packages/logMissingPackages.ts b/src/reporting/packages/logMissingPackages.ts
new file mode 100644
index 0000000..fc7a9e5
--- /dev/null
+++ b/src/reporting/packages/logMissingPackages.ts
@@ -0,0 +1,57 @@
+import chalk from "chalk";
+import { EOL } from "os";
+
+import { Logger } from "../../adapters/logger";
+import { SansDependencies } from "../../binding";
+import { SimplifiedResultsConfiguration } from "../../creation/simplification/simplifyPackageRules";
+import { PackagesConfiguration } from "../../input/findPackagesConfiguration";
+import { isTruthy } from "../../utils";
+import { installationMessages } from "../packages/packageManagers";
+import { choosePackageManager } from "./choosePackageManager";
+
+export type LogMissingPackagesDependencies = {
+    choosePackageManager: SansDependencies<typeof choosePackageManager>;
+    logger: Logger;
+};
+
+export const logMissingPackages = async (
+    dependencies: LogMissingPackagesDependencies,
+    ruleConversionResults: Pick<SimplifiedResultsConfiguration, "extends" | "missing" | "plugins">,
+    packageConfiguration?: PackagesConfiguration,
+) => {
+    const packageManager = await dependencies.choosePackageManager();
+
+    const existingPackageNames = new Set([
+        ...Object.keys(packageConfiguration?.dependencies ?? {}),
+        ...Object.keys(packageConfiguration?.devDependencies ?? {}),
+    ]);
+
+    const requiredPackageNames = [
+        "@typescript-eslint/eslint-plugin",
+        "@typescript-eslint/parser",
+        ruleConversionResults.extends?.join("").includes("eslint-config-prettier") &&
+            "eslint-config-prettier",
+        ruleConversionResults.missing.length !== 0 && "@typescript-eslint/eslint-plugin-tslint",
+        "eslint",
+        ...Array.from(ruleConversionResults.plugins),
+    ].filter(isTruthy);
+
+    const missingPackageNames = requiredPackageNames
+        .filter((packageName) => !existingPackageNames.has(packageName))
+        .sort();
+
+    dependencies.logger.stdout.write(chalk.cyanBright(`${EOL}⚡ ${missingPackageNames.length}`));
+    dependencies.logger.stdout.write(
+        chalk.cyan(
+            ` new package${
+                missingPackageNames.length === 1 ? " is" : "s are"
+            } required for this ESLint configuration.`,
+        ),
+    );
+    dependencies.logger.stdout.write(chalk.cyanBright(" ⚡"));
+    dependencies.logger.stdout.write(`${EOL}  `);
+    dependencies.logger.stdout.write(
+        chalk.cyan(installationMessages[packageManager](missingPackageNames.join(" "))),
+    );
+    dependencies.logger.stdout.write(EOL);
+};
diff --git a/src/reporting/reportConversionResults.ts b/src/reporting/reportConversionResults.ts
index 30f1c3a..7f63938 100644
--- a/src/reporting/reportConversionResults.ts
+++ b/src/reporting/reportConversionResults.ts
@@ -2,20 +2,16 @@ import chalk from "chalk";
 import { EOL } from "os";
 
 import { Logger } from "../adapters/logger";
-import { SansDependencies } from "../binding";
 import { SimplifiedResultsConfiguration } from "../creation/simplification/simplifyPackageRules";
 import { ESLintRuleOptions, TSLintRuleOptions } from "../rules/types";
-import { choosePackageManager } from "./packages/choosePackageManager";
 import {
     logFailedConversions,
     logMissingConversionTarget,
-    logMissingPackages,
     logSuccessfulConversions,
 } from "./reportOutputs";
 
 export type ReportConversionResultsDependencies = {
     logger: Logger;
-    choosePackageManager: SansDependencies<typeof choosePackageManager>;
 };
 
 export const reportConversionResults = async (
@@ -23,8 +19,6 @@ export const reportConversionResults = async (
     outputPath: string,
     ruleConversionResults: SimplifiedResultsConfiguration,
 ) => {
-    const packageManager = await dependencies.choosePackageManager();
-
     if (ruleConversionResults.converted.size !== 0) {
         logSuccessfulConversions("rule", ruleConversionResults.converted, dependencies.logger);
         logNotices(ruleConversionResults.converted, dependencies.logger);
@@ -48,9 +42,7 @@ export const reportConversionResults = async (
         );
     }
 
-    logMissingPackages(ruleConversionResults, packageManager, dependencies.logger);
-
-    if (!ruleConversionResults.extends?.includes("eslint-config-prettier")) {
+    if (!ruleConversionResults.extends?.join("").includes("eslint-config-prettier")) {
         logPrettierExtension(dependencies.logger);
     }
 };
diff --git a/src/reporting/reportOutputs.ts b/src/reporting/reportOutputs.ts
index 138b232..113add2 100644
--- a/src/reporting/reportOutputs.ts
+++ b/src/reporting/reportOutputs.ts
@@ -2,12 +2,9 @@ import chalk from "chalk";
 import { EOL } from "os";
 
 import { Logger } from "../adapters/logger";
-import { SimplifiedResultsConfiguration } from "../creation/simplification/simplifyPackageRules";
 import { EditorSetting } from "../editorSettings/types";
 import { ErrorSummary } from "../errors/errorSummary";
 import { ESLintRuleOptions } from "../rules/types";
-import { uniqueFromSources } from "../utils";
-import { PackageManager, installationMessages } from "./packages/packageManagers";
 
 export type EditorSettingEntry = Pick<EditorSetting, "editorSettingName">;
 
@@ -69,29 +66,3 @@ export const logMissingConversionTarget = <T>(
     );
     logger.info.write(EOL);
 };
-
-export const logMissingPackages = (
-    ruleConversionResults: Pick<SimplifiedResultsConfiguration, "extends" | "missing" | "plugins">,
-    packageManager: PackageManager,
-    logger: Logger,
-) => {
-    const packageNames = uniqueFromSources([
-        "@typescript-eslint/eslint-plugin",
-        "@typescript-eslint/parser",
-        ruleConversionResults.missing.length !== 0 && "@typescript-eslint/eslint-plugin-tslint",
-        "eslint",
-        ...Array.from(
-            ruleConversionResults.extends?.map((extension) => extension.split("/")[0]) ?? [],
-        ),
-        ...Array.from(ruleConversionResults.plugins),
-    ])
-        .filter(Boolean)
-        .sort();
-
-    logger.stdout.write(chalk.cyanBright(`${EOL}⚡ ${packageNames.length}`));
-    logger.stdout.write(chalk.cyan(" packages are required for this ESLint configuration."));
-    logger.stdout.write(chalk.cyanBright(" ⚡"));
-    logger.stdout.write(`${EOL}  `);
-    logger.stdout.write(chalk.cyan(installationMessages[packageManager](packageNames.join(" "))));
-    logger.stdout.write(EOL);
-};
diff --git a/src/utils.ts b/src/utils.ts
index c69d6f9..29c8d5e 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,7 +1,9 @@
-export const isDefined = <Item>(item: Item | undefined): item is Item => !!item;
+export const isDefined = <Item>(item: Item | undefined): item is Item => item !== undefined;
 
 export const isError = <Item>(item: Item | Error): item is Error => item instanceof Error;
 
+export const isTruthy = <Item>(item: Item | false | undefined | null | 0): item is Item => !!item;
+
 export type RemoveErrors<Items> = {
     [P in keyof Items]: Exclude<Items[P], Error>;
 };


Test patch:
diff --git a/src/conversion/convertConfig.test.ts b/src/conversion/convertConfig.test.ts
index c3f4c8b..3fa1e53 100644
--- a/src/conversion/convertConfig.test.ts
+++ b/src/conversion/convertConfig.test.ts
@@ -22,6 +22,7 @@ const createStubDependencies = (
         missing: [],
         plugins: new Set(),
     }),
+    logMissingPackages: jest.fn().mockReturnValue(Promise.resolve()),
     writeConversionResults: jest.fn().mockReturnValue(Promise.resolve()),
     ...overrides,
 });
diff --git a/src/creation/simplification/simplifyPackageRules.test.ts b/src/creation/simplification/simplifyPackageRules.test.ts
index ea119d4..07266de 100644
--- a/src/creation/simplification/simplifyPackageRules.test.ts
+++ b/src/creation/simplification/simplifyPackageRules.test.ts
@@ -68,7 +68,10 @@ describe("simplifyPackageRules", () => {
         expect(simplifiedResults).toEqual({
             ...ruleConversionResults,
             converted: undefined,
-            extends: ["eslint-config-prettier", "eslint-config-prettier/@typescript-eslint"],
+            extends: [
+                "plugin:eslint-config-prettier",
+                "plugin:eslint-config-prettier/@typescript-eslint",
+            ],
         });
     });
 
diff --git a/src/reporting/packages/logMissingPackages.test.ts b/src/reporting/packages/logMissingPackages.test.ts
new file mode 100644
index 0000000..ac1a6fb
--- /dev/null
+++ b/src/reporting/packages/logMissingPackages.test.ts
@@ -0,0 +1,175 @@
+import { createStubLogger, expectEqualWrites } from "../../adapters/logger.stubs";
+import { createEmptyConversionResults } from "../../conversion/conversionResults.stubs";
+import { logMissingPackages } from "./logMissingPackages";
+import { PackageManager } from "./packageManagers";
+
+const createStubDependencies = (packageManager = PackageManager.npm) => ({
+    choosePackageManager: async () => packageManager,
+    logger: createStubLogger(),
+});
+
+describe("logMissingPackages", () => {
+    it("reports a singular message when one package is missing", async () => {
+        // Arrange
+        const { choosePackageManager, logger } = createStubDependencies(PackageManager.npm);
+        const ruleConversionResults = createEmptyConversionResults();
+
+        // Act
+        await logMissingPackages({ choosePackageManager, logger }, ruleConversionResults, {
+            dependencies: {
+                "@typescript-eslint/eslint-plugin": "*",
+                "@typescript-eslint/parser": "*",
+            },
+            devDependencies: {},
+        });
+
+        // Assert
+        expectEqualWrites(
+            logger.stdout.write,
+            `⚡ 1 new package is required for this ESLint configuration. ⚡`,
+            `  npm install eslint --save-dev`,
+        );
+    });
+
+    it("does not include eslint-config-prettier when there are no extensions", async () => {
+        // Arrange
+        const { choosePackageManager, logger } = createStubDependencies();
+        const ruleConversionResults = createEmptyConversionResults({
+            extends: undefined,
+        });
+
+        // Act
+        await logMissingPackages({ choosePackageManager, logger }, ruleConversionResults);
+
+        // Assert
+        expectEqualWrites(
+            logger.stdout.write,
+            `⚡ 3 new packages are required for this ESLint configuration. ⚡`,
+            `  npm install @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint --save-dev`,
+        );
+    });
+
+    it("does not include eslint-config-prettier when extensions don't include eslint-config-prettier", async () => {
+        // Arrange
+        const { choosePackageManager, logger } = createStubDependencies();
+        const ruleConversionResults = createEmptyConversionResults({
+            extends: [],
+        });
+
+        // Act
+        await logMissingPackages({ choosePackageManager, logger }, ruleConversionResults);
+
+        // Assert
+        expectEqualWrites(
+            logger.stdout.write,
+            `⚡ 3 new packages are required for this ESLint configuration. ⚡`,
+            `  npm install @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint --save-dev`,
+        );
+    });
+
+    it("includes eslint-config-prettier when extensions include eslint-config-prettier", async () => {
+        // Arrange
+        const { choosePackageManager, logger } = createStubDependencies();
+        const ruleConversionResults = createEmptyConversionResults({
+            extends: ["plugin:eslint-config-prettier"],
+        });
+
+        // Act
+        await logMissingPackages({ choosePackageManager, logger }, ruleConversionResults);
+
+        // Assert
+        expectEqualWrites(
+            logger.stdout.write,
+            `⚡ 4 new packages are required for this ESLint configuration. ⚡`,
+            `  npm install @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-prettier --save-dev`,
+        );
+    });
+
+    it("includes @typescript-eslint/eslint-plugin-tslint when there are missing conversions", async () => {
+        // Arrange
+        const { choosePackageManager, logger } = createStubDependencies();
+        const ruleConversionResults = createEmptyConversionResults({
+            missing: [
+                {
+                    ruleArguments: [],
+                    ruleName: "missing-rule",
+                    ruleSeverity: "error",
+                },
+            ],
+        });
+
+        // Act
+        await logMissingPackages({ choosePackageManager, logger }, ruleConversionResults);
+
+        // Assert
+        expectEqualWrites(
+            logger.stdout.write,
+            `⚡ 4 new packages are required for this ESLint configuration. ⚡`,
+            `  npm install @typescript-eslint/eslint-plugin @typescript-eslint/eslint-plugin-tslint @typescript-eslint/parser eslint --save-dev`,
+        );
+    });
+
+    it("reports an npm command when the package manager is npm", async () => {
+        // Arrange
+        const { choosePackageManager, logger } = createStubDependencies(PackageManager.npm);
+        const ruleConversionResults = createEmptyConversionResults();
+
+        // Act
+        await logMissingPackages({ choosePackageManager, logger }, ruleConversionResults);
+
+        // Assert
+        expectEqualWrites(
+            logger.stdout.write,
+            `⚡ 3 new packages are required for this ESLint configuration. ⚡`,
+            `  npm install @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint --save-dev`,
+        );
+    });
+
+    it("reports a pnpm command when the package manager is pnpm", async () => {
+        // Arrange
+        const { choosePackageManager, logger } = createStubDependencies(PackageManager.pnpm);
+        const ruleConversionResults = createEmptyConversionResults();
+
+        // Act
+        await logMissingPackages({ choosePackageManager, logger }, ruleConversionResults);
+
+        // Assert
+        expectEqualWrites(
+            logger.stdout.write,
+            `⚡ 3 new packages are required for this ESLint configuration. ⚡`,
+            `  pnpm add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint --save-dev`,
+        );
+    });
+
+    it("reports a Yarn command when the package manager is Yarn", async () => {
+        // Arrange
+        const { choosePackageManager, logger } = createStubDependencies(PackageManager.Yarn);
+        const ruleConversionResults = createEmptyConversionResults();
+
+        // Act
+        await logMissingPackages({ choosePackageManager, logger }, ruleConversionResults);
+
+        // Assert
+        expectEqualWrites(
+            logger.stdout.write,
+            `⚡ 3 new packages are required for this ESLint configuration. ⚡`,
+            `  yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint --dev`,
+        );
+    });
+
+    it("reports a Yarn command when the package manager is Yarn", async () => {
+        // Arrange
+        const { choosePackageManager, logger } = createStubDependencies(PackageManager.Yarn);
+        const ruleConversionResults = createEmptyConversionResults();
+
+        // Act
+        await logMissingPackages({ choosePackageManager, logger }, ruleConversionResults);
+
+        // Assert
+        expectEqualWrites(
+            logger.stdout.write,
+            `⚡ 3 new packages are required for this ESLint configuration. ⚡`,
+            `  yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint --dev`,
+        );
+    });
+});
diff --git a/src/reporting/reportConversionResults.test.ts b/src/reporting/reportConversionResults.test.ts
index 0b6a6e1..4da981b 100644
--- a/src/reporting/reportConversionResults.test.ts
+++ b/src/reporting/reportConversionResults.test.ts
@@ -3,21 +3,17 @@ import { EOL } from "os";
 import { createStubLogger, expectEqualWrites } from "../adapters/logger.stubs";
 import { createEmptyConversionResults } from "../conversion/conversionResults.stubs";
 import { ESLintRuleOptions } from "../rules/types";
-import { PackageManager } from "./packages/packageManagers";
 import { reportConversionResults } from "./reportConversionResults";
 
-const createStubDependencies = (packageManager = PackageManager.Yarn) => {
-    const choosePackageManager = jest.fn().mockResolvedValueOnce(packageManager);
-    const logger = createStubLogger();
-
-    return { choosePackageManager, logger };
-};
-
-const basicExtends = ["eslint-config-prettier", "eslint-config-prettier/@typescript-eslint"];
+const basicExtends = [
+    "plugin:eslint-config-prettier",
+    "plugin:eslint-config-prettier/@typescript-eslint",
+];
 
 describe("reportConversionResults", () => {
     it("logs a successful conversion without notices when there is one converted rule without notices", async () => {
         // Arrange
+        const logger = createStubLogger();
         const conversionResults = createEmptyConversionResults({
             converted: new Map<string, ESLintRuleOptions>([
                 [
@@ -32,27 +28,16 @@ describe("reportConversionResults", () => {
             extends: basicExtends,
         });
 
-        const { choosePackageManager, logger } = createStubDependencies();
-
         // Act
-        await reportConversionResults(
-            { choosePackageManager, logger },
-            ".eslintrc.js",
-            conversionResults,
-        );
+        await reportConversionResults({ logger }, ".eslintrc.js", conversionResults);
 
         // Assert
-        expectEqualWrites(
-            logger.stdout.write,
-            `✨ 1 rule replaced with its ESLint equivalent. ✨`,
-            ``,
-            `⚡ 4 packages are required for this ESLint configuration. ⚡`,
-            `  yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-prettier --dev`,
-        );
+        expectEqualWrites(logger.stdout.write, `✨ 1 rule replaced with its ESLint equivalent. ✨`);
     });
 
     it("logs a successful conversion with notices when there is one converted rule with notices", async () => {
         // Arrange
+        const logger = createStubLogger();
         const conversionResults = createEmptyConversionResults({
             converted: new Map<string, ESLintRuleOptions>([
                 [
@@ -68,14 +53,8 @@ describe("reportConversionResults", () => {
             extends: basicExtends,
         });
 
-        const { choosePackageManager, logger } = createStubDependencies();
-
         // Act
-        await reportConversionResults(
-            { choosePackageManager, logger },
-            ".eslintrc.js",
-            conversionResults,
-        );
+        await reportConversionResults({ logger }, ".eslintrc.js", conversionResults);
 
         // Assert
         expectEqualWrites(
@@ -83,9 +62,6 @@ describe("reportConversionResults", () => {
             `✨ 1 rule replaced with its ESLint equivalent. ✨${EOL}`,
             `❗ 1 ESLint rule behaves differently from its TSLint counterpart ❗`,
             `  Check ${logger.debugFileName} for details.`,
-            ``,
-            `⚡ 4 packages are required for this ESLint configuration. ⚡`,
-            `  yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-prettier --dev`,
         );
         expectEqualWrites(
             logger.info.write,
@@ -98,6 +74,7 @@ describe("reportConversionResults", () => {
 
     it("logs successful conversions when there are multiple converted rules", async () => {
         // Arrange
+        const logger = createStubLogger();
         const conversionResults = createEmptyConversionResults({
             converted: new Map<string, ESLintRuleOptions>([
                 [
@@ -122,14 +99,8 @@ describe("reportConversionResults", () => {
             extends: basicExtends,
         });
 
-        const { choosePackageManager, logger } = createStubDependencies();
-
         // Act
-        await reportConversionResults(
-            { choosePackageManager, logger },
-            ".eslintrc.js",
-            conversionResults,
-        );
+        await reportConversionResults({ logger }, ".eslintrc.js", conversionResults);
 
         // Assert
         expectEqualWrites(
@@ -138,9 +109,6 @@ describe("reportConversionResults", () => {
             ``,
             `❗ 2 ESLint rules behave differently from their TSLint counterparts ❗`,
             `  Check ${logger.debugFileName} for details.`,
-            ``,
-            `⚡ 4 packages are required for this ESLint configuration. ⚡`,
-            `  yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-prettier --dev`,
         );
         expectEqualWrites(
             logger.info.write,
@@ -156,19 +124,14 @@ describe("reportConversionResults", () => {
 
     it("logs a failed conversion when there is one failed conversion", async () => {
         // Arrange
+        const logger = createStubLogger();
         const conversionResults = createEmptyConversionResults({
             failed: [{ getSummary: () => "It broke." }],
             extends: basicExtends,
         });
 
-        const { choosePackageManager, logger } = createStubDependencies();
-
         // Act
-        await reportConversionResults(
-            { choosePackageManager, logger },
-            ".eslintrc.js",
-            conversionResults,
-        );
+        await reportConversionResults({ logger }, ".eslintrc.js", conversionResults);
 
         // Assert
         expectEqualWrites(
@@ -180,19 +143,14 @@ describe("reportConversionResults", () => {
 
     it("logs failed conversions when there are multiple failed conversions", async () => {
         // Arrange
+        const logger = createStubLogger();
         const conversionResults = createEmptyConversionResults({
             extends: basicExtends,
             failed: [{ getSummary: () => "It broke." }, { getSummary: () => "It really broke." }],
         });
 
-        const { choosePackageManager, logger } = createStubDependencies();
-
         // Act
-        await reportConversionResults(
-            { choosePackageManager, logger },
-            ".eslintrc.js",
-            conversionResults,
-        );
+        await reportConversionResults({ logger }, ".eslintrc.js", conversionResults);
 
         // Assert
         expectEqualWrites(
@@ -204,6 +162,7 @@ describe("reportConversionResults", () => {
 
     it("logs a missing rule when there is a missing rule", async () => {
         // Arrange
+        const logger = createStubLogger();
         const conversionResults = createEmptyConversionResults({
             extends: basicExtends,
             missing: [
@@ -215,14 +174,8 @@ describe("reportConversionResults", () => {
             ],
         });
 
-        const { choosePackageManager, logger } = createStubDependencies();
-
         // Act
-        await reportConversionResults(
-            { choosePackageManager, logger },
-            ".eslintrc.js",
-            conversionResults,
-        );
+        await reportConversionResults({ logger }, ".eslintrc.js", conversionResults);
 
         // Assert
         expectEqualWrites(
@@ -230,9 +183,6 @@ describe("reportConversionResults", () => {
             `❓ 1 rule is not known by tslint-to-eslint-config to have an ESLint equivalent. ❓`,
             `  The "@typescript-eslint/tslint/config" section of .eslintrc.js configures eslint-plugin-tslint to run it in TSLint within ESLint.`,
             `  Check ${logger.debugFileName} for details.`,
-            ``,
-            `⚡ 5 packages are required for this ESLint configuration. ⚡`,
-            `  yarn add @typescript-eslint/eslint-plugin @typescript-eslint/eslint-plugin-tslint @typescript-eslint/parser eslint eslint-config-prettier --dev`,
         );
         expectEqualWrites(
             logger.info.write,
@@ -243,6 +193,7 @@ describe("reportConversionResults", () => {
 
     it("logs missing rules when there are missing rules", async () => {
         // Arrange
+        const logger = createStubLogger();
         const conversionResults = createEmptyConversionResults({
             extends: basicExtends,
             missing: [
@@ -259,14 +210,8 @@ describe("reportConversionResults", () => {
             ],
         });
 
-        const { choosePackageManager, logger } = createStubDependencies();
-
         // Act
-        await reportConversionResults(
-            { choosePackageManager, logger },
-            ".eslintrc.js",
-            conversionResults,
-        );
+        await reportConversionResults({ logger }, ".eslintrc.js", conversionResults);
 
         // Assert
         expectEqualWrites(
@@ -274,9 +219,6 @@ describe("reportConversionResults", () => {
             `❓ 2 rules are not known by tslint-to-eslint-config to have ESLint equivalents. ❓`,
             `  The "@typescript-eslint/tslint/config" section of .eslintrc.js configures eslint-plugin-tslint to run them in TSLint within ESLint.`,
             `  Check ${logger.debugFileName} for details.`,
-            ``,
-            `⚡ 5 packages are required for this ESLint configuration. ⚡`,
-            `  yarn add @typescript-eslint/eslint-plugin @typescript-eslint/eslint-plugin-tslint @typescript-eslint/parser eslint eslint-config-prettier --dev`,
         );
         expectEqualWrites(
             logger.info.write,
@@ -286,51 +228,38 @@ describe("reportConversionResults", () => {
         );
     });
 
-    it("logs missing plugins when there are missing plugins", async () => {
+    it("logs a Prettier recommendation when there are no extensions", async () => {
         // Arrange
+        const logger = createStubLogger();
         const conversionResults = createEmptyConversionResults({
-            extends: basicExtends,
-            plugins: new Set(["plugin-one", "plugin-two"]),
+            extends: undefined,
         });
 
-        const { choosePackageManager, logger } = createStubDependencies();
-
         // Act
-        await reportConversionResults(
-            { choosePackageManager, logger },
-            ".eslintrc.js",
-            conversionResults,
-        );
+        await reportConversionResults({ logger }, ".eslintrc.js", conversionResults);
 
         // Assert
         expectEqualWrites(
             logger.stdout.write,
-            `⚡ 6 packages are required for this ESLint configuration. ⚡`,
-            `  yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-prettier plugin-one plugin-two --dev`,
+            `☠ Prettier plugins are missing from your configuration. ☠`,
+            `  We highly recommend running tslint-to-eslint-config --prettier to disable formatting ESLint rules.`,
+            `  See https://github/typescript-eslint/tslint-to-eslint-config/docs/FAQs.md#should-i-use-prettier.`,
         );
     });
 
-    it("logs a Prettier recommendation when eslint-config-prettier isn't extended", async () => {
+    it("logs a Prettier recommendation when extends don't include eslint-config-prettier", async () => {
         // Arrange
+        const logger = createStubLogger();
         const conversionResults = createEmptyConversionResults({
             extends: [],
         });
 
-        const { choosePackageManager, logger } = createStubDependencies();
-
         // Act
-        await reportConversionResults(
-            { choosePackageManager, logger },
-            ".eslintrc.js",
-            conversionResults,
-        );
+        await reportConversionResults({ logger }, ".eslintrc.js", conversionResults);
 
         // Assert
         expectEqualWrites(
             logger.stdout.write,
-            `⚡ 3 packages are required for this ESLint configuration. ⚡`,
-            `  yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint --dev`,
-            ``,
             `☠ Prettier plugins are missing from your configuration. ☠`,
             `  We highly recommend running tslint-to-eslint-config --prettier to disable formatting ESLint rules.`,
             `  See https://github/typescript-eslint/tslint-to-eslint-config/docs/FAQs.md#should-i-use-prettier.`,
diff --git a/src/reporting/reportOutputs.test.ts b/src/reporting/reportOutputs.test.ts
deleted file mode 100644
index 4e7f01c..0000000
--- a/src/reporting/reportOutputs.test.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import { createStubLogger, expectEqualWrites } from "../adapters/logger.stubs";
-import { createEmptyConversionResults } from "../conversion/conversionResults.stubs";
-import { PackageManager } from "./packages/packageManagers";
-import { logMissingPackages } from "./reportOutputs";
-import { SimplifiedResultsConfiguration } from "../creation/simplification/simplifyPackageRules";
-
-const createStubDependencies = (
-    packageManager: PackageManager,
-    results?: Partial<SimplifiedResultsConfiguration>,
-) => ({
-    logger: createStubLogger(),
-    packageManager,
-    ruleConversionResults: createEmptyConversionResults(results),
-});
-
-describe("reportOutputs", () => {
-    it("reports an npm command when the package manager is npm", () => {
-        // Arrange
-        createEmptyConversionResults();
-        const { logger, packageManager, ruleConversionResults } = createStubDependencies(
-            PackageManager.npm,
-        );
-
-        // Act
-        logMissingPackages(ruleConversionResults, packageManager, logger);
-
-        // Assert
-        expectEqualWrites(
-            logger.stdout.write,
-            `⚡ 3 packages are required for this ESLint configuration. ⚡`,
-            `  npm install @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint --save-dev`,
-        );
-    });
-
-    it("reports a pnpm command when the package manager is pnpm", () => {
-        // Arrange
-        const { logger, packageManager, ruleConversionResults } = createStubDependencies(
-            PackageManager.pnpm,
-        );
-
-        // Act
-        logMissingPackages(ruleConversionResults, packageManager, logger);
-
-        // Assert
-        expectEqualWrites(
-            logger.stdout.write,
-            `⚡ 3 packages are required for this ESLint configuration. ⚡`,
-            `  pnpm add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint --save-dev`,
-        );
-    });
-
-    it("reports a Yarn command when the package manager is Yarn", () => {
-        // Arrange
-        const { logger, packageManager, ruleConversionResults } = createStubDependencies(
-            PackageManager.Yarn,
-        );
-
-        // Act
-        logMissingPackages(ruleConversionResults, packageManager, logger);
-
-        // Assert
-        expectEqualWrites(
-            logger.stdout.write,
-            `⚡ 3 packages are required for this ESLint configuration. ⚡`,
-            `  yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint --dev`,
-        );
-    });
-
-    it("adds extensions to the missing packages list when they exist", () => {
-        // Arrange
-        const { logger, packageManager, ruleConversionResults } = createStubDependencies(
-            PackageManager.Yarn,
-            {
-                extends: ["eslint-config-prettier", "eslint-config-prettier/@typescript-eslint"],
-            },
-        );
-
-        // Act
-        logMissingPackages(ruleConversionResults, packageManager, logger);
-
-        // Assert
-        expectEqualWrites(
-            logger.stdout.write,
-            `⚡ 4 packages are required for this ESLint configuration. ⚡`,
-            `  yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-prettier --dev`,
-        );
-    });
-});