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

Issue:
Map from TSLint to @typescript-eslint rulesets
### 🚀 Feature Request

When a TSLInt configuration includes `"extends": ["tslint:recommended"]`, one would expect a generated ESLint configuration to `extends: ["plugin:@typescript-eslint/recommended-requiring-type-checking"]`. 

#### Existing Behavior

No output extends configurations are generated right now. It's all `rules`.

#### Change Proposal

If a TSLint configuration extends from a known built-in configuration, the output ESLint configuration should extend from the equivalent ESLint / typescript-eslint one _if_ it exists.

Pull request description:
Account for TSLint and @typescript-eslint rulesets
## PR Checklist

-   [x] Addresses an existing issue: fixes #298
-   [x] That issue was marked as [`status: accepting prs`](https://github.com/typescript-eslint/tslint-to-eslint-config/labels/status%3A%20accepting%20prs)

## Overview

When an input TSLint configuration directly `extends` from a built-in preset, that preset is added to the list of `extends` in the ESLint output. Rules already enabled with the same settings in those extensions should, as per existing behavior, not be redundantly enabled/configured in the ESLint output.

Patch:
diff --git a/README.md b/README.md
index 621cbe9..c8a89e4 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
 [![Join the chat at https://gitter.im/tslint-to-eslint-config/community](https://img.shields.io/badge/chat-gitter-informational.svg)](https://gitter.im/tslint-to-eslint-config/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
 [![Code Style: Prettier](https://img.shields.io/badge/speed-blazingly_fast-blueviolet.svg)](https://prettier.io)
 
-Converts your TSLint configuration to the closest possible ESLint equivalent.
+Converts your TSLint configuration to the closest reasonable ESLint equivalent.
 
 👉 Did you know [TSLint is being deprecated this year](https://github.com/palantir/tslint/issues/4534)?
 Hooray!
diff --git a/docs/Architecture.md b/docs/Architecture.md
index 00ddfe4..ec2224a 100644
--- a/docs/Architecture.md
+++ b/docs/Architecture.md
@@ -13,7 +13,7 @@ Within `src/conversion/convertConfig.ts`, the following steps occur:
 
 1. Existing configurations are read from disk
 2. TSLint rules are converted into their ESLint configurations
-3. ESLint configurations are simplified based on extended ESLint presets
+3. ESLint configurations are simplified based on extended ESLint and TSLint presets
 4. The simplified configuration is written to the output config file
 5. A summary of the results is printed to the user's console
 
diff --git a/package.json b/package.json
index ffb86d1..3596b4c 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
     "bugs": {
         "url": "https://github.com/typescript-eslint/tslint-to-eslint-config/issues"
     },
-    "description": "Converts your TSLint configuration to the closest possible ESLint equivalent.",
+    "description": "Converts your TSLint configuration to the closest reasonable ESLint equivalent.",
     "dependencies": {
         "chalk": "3.0.0",
         "commander": "4.0.1",
diff --git a/src/conversion/convertConfig.ts b/src/conversion/convertConfig.ts
index 17195d7..c016a0b 100644
--- a/src/conversion/convertConfig.ts
+++ b/src/conversion/convertConfig.ts
@@ -33,11 +33,12 @@ export const convertConfig = async (
         originalConfigurations.data.tslint.full.rules,
     );
 
-    // 3. ESLint configurations are simplified based on extended ESLint presets
+    // 3. ESLint configurations are simplified based on extended ESLint and TSLint presets
     const simplifiedConfiguration = {
         ...ruleConversionResults,
         ...(await dependencies.simplifyPackageRules(
             originalConfigurations.data.eslint,
+            originalConfigurations.data.tslint,
             ruleConversionResults,
         )),
     };
diff --git a/src/creation/simplification/collectTSLintRulesets.ts b/src/creation/simplification/collectTSLintRulesets.ts
new file mode 100644
index 0000000..66f8685
--- /dev/null
+++ b/src/creation/simplification/collectTSLintRulesets.ts
@@ -0,0 +1,34 @@
+import { OriginalConfigurations } from "../../input/findOriginalConfigurations";
+import { TSLintConfiguration } from "../../input/findTSLintConfiguration";
+import { uniqueFromSources } from "../../utils";
+
+const nativeExtensions = new Map([
+    ["tslint:all", ["plugin:@typescript-eslint/all"]],
+    [
+        "tslint:recommended",
+        [
+            "plugin:@typescript-eslint/recommended",
+            "plugin:@typescript-eslint/recommended-requiring-type-checking",
+        ],
+    ],
+]);
+
+export const collectTSLintRulesets = (
+    tslint: OriginalConfigurations<Pick<TSLintConfiguration, "extends">>,
+) => {
+    const allExtensions = uniqueFromSources(tslint.full.extends, tslint.raw.extends);
+
+    const extensions = new Set<string>();
+
+    for (const extension of allExtensions) {
+        const mappedExtensions = nativeExtensions.get(extension);
+
+        if (mappedExtensions !== undefined) {
+            for (const mappedExtension of mappedExtensions) {
+                extensions.add(mappedExtension);
+            }
+        }
+    }
+
+    return Array.from(extensions);
+};
diff --git a/src/creation/simplification/resolveExtensionNames.ts b/src/creation/simplification/resolveExtensionNames.ts
index 7b1b1b6..028219d 100644
--- a/src/creation/simplification/resolveExtensionNames.ts
+++ b/src/creation/simplification/resolveExtensionNames.ts
@@ -6,7 +6,8 @@ export const resolveExtensionNames = (rawExtensionNames: string | string[]) => {
     return rawExtensionNames.map(rawExtensionName =>
         rawExtensionName.startsWith(".") ||
         rawExtensionName.startsWith("eslint-plugin-") ||
-        rawExtensionName.startsWith("eslint:")
+        rawExtensionName.startsWith("eslint:") ||
+        rawExtensionName.startsWith("plugin:")
             ? rawExtensionName
             : `eslint-plugin-${rawExtensionName}`,
     );
diff --git a/src/creation/simplification/retrieveExtendsValues.ts b/src/creation/simplification/retrieveExtendsValues.ts
index 6138d23..fda3cfc 100644
--- a/src/creation/simplification/retrieveExtendsValues.ts
+++ b/src/creation/simplification/retrieveExtendsValues.ts
@@ -13,6 +13,29 @@ export type RetrievedExtensionValues = {
     importedExtensions: Partial<ESLintConfiguration>[];
 };
 
+const builtInExtensions = new Map([
+    ["eslint:all", "eslint/conf/eslint-all"],
+    ["eslint:recommended", "eslint/conf/eslint-recommended"],
+]);
+
+const typescriptPluginExtensions = new Map([
+    [
+        "plugin:@typescript-eslint/all",
+        "node_modules/@typescript-eslint/eslint-plugin/dist/configs/all.json",
+    ],
+    [
+        "plugin:@typescript-eslint/recommended",
+        "node_modules/@typescript-eslint/eslint-plugin/dist/configs/recommended.json",
+    ],
+    [
+        "plugin:@typescript-eslint/recommended-requiring-type-checking",
+        "node_modules/@typescript-eslint/eslint-plugin/dist/configs/recommended-requiring-type-checking.json",
+    ],
+]);
+
+/**
+ * Imports any extended ESLint rulesets as ESLint configurations.
+ */
 export const retrieveExtendsValues = async (
     dependencies: RetrieveExtendsValuesDependencies,
     rawExtensionNames: string | string[],
@@ -21,26 +44,24 @@ export const retrieveExtendsValues = async (
     const configurationErrors: ConfigurationError[] = [];
     const extensionNames = resolveExtensionNames(rawExtensionNames);
 
-    const builtInExtensionGetters = new Map<string, () => Promise<ESLintConfiguration>>([
-        [
-            "eslint:all",
-            async () =>
-                (await dependencies.importer("eslint/conf/eslint-all")) as ESLintConfiguration,
-        ],
-        [
-            "eslint:recommended",
-            async () =>
-                (await dependencies.importer(
-                    "eslint/conf/eslint-recommended",
-                )) as ESLintConfiguration,
-        ],
-    ]);
-
     await Promise.all(
         extensionNames.map(async extensionName => {
-            const getBuiltInExtension = builtInExtensionGetters.get(extensionName);
-            if (getBuiltInExtension !== undefined) {
-                importedExtensions.push(await getBuiltInExtension());
+            const builtInExtension = builtInExtensions.get(extensionName);
+            if (builtInExtension !== undefined) {
+                importedExtensions.push(
+                    (await dependencies.importer(builtInExtension)) as ESLintConfiguration,
+                );
+                return;
+            }
+
+            const typescriptPluginExtension = typescriptPluginExtensions.get(extensionName);
+            if (typescriptPluginExtension !== undefined) {
+                const importedTypeScriptPlugin = (await dependencies.importer(
+                    typescriptPluginExtension,
+                )) as ESLintConfiguration;
+                importedExtensions.push({
+                    rules: importedTypeScriptPlugin.rules,
+                });
                 return;
             }
 
diff --git a/src/creation/simplification/simplifyPackageRules.ts b/src/creation/simplification/simplifyPackageRules.ts
index b813ddd..b7b9d8b 100644
--- a/src/creation/simplification/simplifyPackageRules.ts
+++ b/src/creation/simplification/simplifyPackageRules.ts
@@ -1,28 +1,44 @@
 import { SansDependencies } from "../../binding";
+import { ESLintConfiguration } from "../../input/findESLintConfiguration";
+import { OriginalConfigurations } from "../../input/findOriginalConfigurations";
+import { TSLintConfiguration } from "../../input/findTSLintConfiguration";
 import { RuleConversionResults } from "../../rules/convertRules";
+import { uniqueFromSources } from "../../utils";
+import { collectTSLintRulesets } from "./collectTSLintRulesets";
 import { removeExtendsDuplicatedRules } from "./removeExtendsDuplicatedRules";
 import { retrieveExtendsValues } from "./retrieveExtendsValues";
-import { ESLintConfiguration } from "../../input/findESLintConfiguration";
-import { OriginalConfigurations } from "../../input/findOriginalConfigurations";
 
 export type SimplifyPackageRulesDependencies = {
     removeExtendsDuplicatedRules: typeof removeExtendsDuplicatedRules;
     retrieveExtendsValues: SansDependencies<typeof retrieveExtendsValues>;
 };
 
-export type SimplifiedRuleConversionResults = Pick<RuleConversionResults, "converted" | "failed">;
+export type SimplifiedRuleConversionResults = Pick<
+    RuleConversionResults,
+    "converted" | "failed"
+> & {
+    extends?: string[];
+};
 
+/**
+ * Given an initial set of rule conversion results and original configurations,
+ * determines which ESLint rulesets to extend from and removes redundant rule values.
+ */
 export const simplifyPackageRules = async (
     dependencies: SimplifyPackageRulesDependencies,
     eslint: Pick<OriginalConfigurations<ESLintConfiguration>, "full"> | undefined,
+    tslint: OriginalConfigurations<Pick<TSLintConfiguration, "extends">>,
     ruleConversionResults: SimplifiedRuleConversionResults,
 ): Promise<SimplifiedRuleConversionResults> => {
-    if (eslint === undefined || eslint.full.extends.length === 0) {
+    const extendedESLintRulesets = eslint?.full.extends ?? [];
+    const extendedTSLintRulesets = collectTSLintRulesets(tslint);
+    const allExtensions = uniqueFromSources(extendedESLintRulesets, extendedTSLintRulesets);
+    if (allExtensions.length === 0) {
         return ruleConversionResults;
     }
 
     const { configurationErrors, importedExtensions } = await dependencies.retrieveExtendsValues(
-        eslint.full.extends,
+        uniqueFromSources(extendedESLintRulesets, extendedTSLintRulesets),
     );
 
     const converted = dependencies.removeExtendsDuplicatedRules(
@@ -32,6 +48,7 @@ export const simplifyPackageRules = async (
 
     return {
         converted,
+        extends: allExtensions,
         failed: [...ruleConversionResults.failed, ...configurationErrors],
     };
 };
diff --git a/src/creation/writeConversionResults.ts b/src/creation/writeConversionResults.ts
index 531c7aa..de39fb2 100644
--- a/src/creation/writeConversionResults.ts
+++ b/src/creation/writeConversionResults.ts
@@ -1,9 +1,10 @@
 import { FileSystem } from "../adapters/fileSystem";
 import { RuleConversionResults } from "../rules/convertRules";
-import { formatConvertedRules } from "./formatConvertedRules";
 import { AllOriginalConfigurations } from "../input/findOriginalConfigurations";
 import { createEnv } from "./eslint/createEnv";
+import { formatConvertedRules } from "./formatConvertedRules";
 import { formatOutput } from "./formatting/formatOutput";
+import { SimplifiedRuleConversionResults } from "./simplification/simplifyPackageRules";
 
 export type WriteConversionResultsDependencies = {
     fileSystem: Pick<FileSystem, "writeFile">;
@@ -12,7 +13,7 @@ export type WriteConversionResultsDependencies = {
 export const writeConversionResults = async (
     dependencies: WriteConversionResultsDependencies,
     outputPath: string,
-    ruleConversionResults: RuleConversionResults,
+    ruleConversionResults: RuleConversionResults & SimplifiedRuleConversionResults,
     originalConfigurations: AllOriginalConfigurations,
 ) => {
     const plugins = ["@typescript-eslint"];
@@ -26,6 +27,7 @@ export const writeConversionResults = async (
         ...eslint?.full,
         env: createEnv(originalConfigurations),
         ...(eslint && { globals: eslint.raw.globals }),
+        ...(ruleConversionResults.extends && { extends: ruleConversionResults.extends }),
         parser: "@typescript-eslint/parser",
         parserOptions: {
             project: "tsconfig.json",
diff --git a/src/input/findOriginalConfigurations.ts b/src/input/findOriginalConfigurations.ts
index fcfe101..b112aae 100644
--- a/src/input/findOriginalConfigurations.ts
+++ b/src/input/findOriginalConfigurations.ts
@@ -51,7 +51,7 @@ export const findOriginalConfigurations = async (
         dependencies.findTypeScriptConfiguration(rawSettings.typescript),
     ]);
 
-    // Out of those configurations, only TSLint's is required to run
+    // Out of those configurations, only TSLint's is always required to run
     if (tslint instanceof Error) {
         return {
             complaints: [tslint.message],
diff --git a/src/input/importer.ts b/src/input/importer.ts
index 03955ff..360e281 100644
--- a/src/input/importer.ts
+++ b/src/input/importer.ts
@@ -10,7 +10,10 @@ export type ImporterDependencies = {
     nativeImporter: NativeImporter;
 };
 
-export const importer = async (dependencies: ImporterDependencies, moduleName: string) => {
+export const importer = async (
+    dependencies: ImporterDependencies,
+    moduleName: string,
+): Promise<any | Error> => {
     const pathAttempts = [path.join(dependencies.getCwd(), moduleName), moduleName];
 
     const importFile = async (filePath: string) => {
diff --git a/src/reporting/reportOutputs.ts b/src/reporting/reportOutputs.ts
index 67af19d..3694b1d 100644
--- a/src/reporting/reportOutputs.ts
+++ b/src/reporting/reportOutputs.ts
@@ -28,7 +28,7 @@ export const logFailedConversions = (failed: ErrorSummary[], logger: Logger) =>
     logger.stderr.write(chalk.red(" thrown."));
     logger.stderr.write(chalk.redBright(` 💀${EOL}`));
 
-    logger.info.write(failed.map(failed => failed.getSummary()).join(""));
+    logger.info.write(failed.map(failed => failed.getSummary()).join("\n\n") + "\n\n");
 
     logger.stderr.write(chalk.gray(`Check ${logger.debugFileName} for details.${EOL}`));
 };


Test patch:
diff --git a/src/conversion/convertConfig.test.ts b/src/conversion/convertConfig.test.ts
index 8a456bb..66f202d 100644
--- a/src/conversion/convertConfig.test.ts
+++ b/src/conversion/convertConfig.test.ts
@@ -14,7 +14,11 @@ const createStubDependencies = (
         status: ResultStatus.Succeeded,
     }),
     reportConversionResults: jest.fn(),
-    simplifyPackageRules: async (_configurations, data) => data,
+    simplifyPackageRules: async (_configurations, data) => ({
+        ...data,
+        converted: new Map(),
+        failed: [],
+    }),
     writeConversionResults: jest.fn().mockReturnValue(Promise.resolve()),
     ...overrides,
 });
diff --git a/src/creation/simplification/collectTSLintRulesets.test.ts b/src/creation/simplification/collectTSLintRulesets.test.ts
new file mode 100644
index 0000000..97822ef
--- /dev/null
+++ b/src/creation/simplification/collectTSLintRulesets.test.ts
@@ -0,0 +1,50 @@
+import { collectTSLintRulesets } from "./collectTSLintRulesets";
+
+describe("collectTSLintRulesets", () => {
+    it("includes mapped ESLint extensions for a full TSLint extension when it exists", () => {
+        const tslint = {
+            full: {
+                extends: ["tslint:recommended"],
+            },
+            raw: {},
+        };
+
+        const extensions = collectTSLintRulesets(tslint);
+
+        expect(extensions).toEqual([
+            "plugin:@typescript-eslint/recommended",
+            "plugin:@typescript-eslint/recommended-requiring-type-checking",
+        ]);
+    });
+
+    it("includes mapped ESLint extensions for a raw TSLint extension when it exists", () => {
+        const tslint = {
+            full: {},
+            raw: {
+                extends: ["tslint:recommended"],
+            },
+        };
+
+        const extensions = collectTSLintRulesets(tslint);
+
+        expect(extensions).toEqual([
+            "plugin:@typescript-eslint/recommended",
+            "plugin:@typescript-eslint/recommended-requiring-type-checking",
+        ]);
+    });
+
+    it("ignores a TSLint extension when it has no mapped ESLint extensions", () => {
+        const tslint = {
+            full: {
+                extends: ["does not exist"],
+            },
+            raw: {
+                extends: ["also does not exist"],
+            },
+        };
+
+        const extensions = collectTSLintRulesets(tslint);
+
+        expect(extensions).toEqual([]);
+    });
+});
diff --git a/src/creation/simplification/resolveExtensionNames.test.ts b/src/creation/simplification/resolveExtensionNames.test.ts
index 5fa8322..6a8e7d2 100644
--- a/src/creation/simplification/resolveExtensionNames.test.ts
+++ b/src/creation/simplification/resolveExtensionNames.test.ts
@@ -55,6 +55,7 @@ describe("resolveExtensionNames", () => {
         // Assert
         expect(extensionNames).toEqual(["eslint-plugin-value"]);
     });
+
     it("doesn't prepend eslint-plugin- when a plugin starts with eslint:", () => {
         // Arrange
         const rawExtensionName = "eslint:recommended";
@@ -65,4 +66,15 @@ describe("resolveExtensionNames", () => {
         // Assert
         expect(extensionNames).toEqual(["eslint:recommended"]);
     });
+
+    it("doesn't prepend eslint-plugin- when a plugin starts with plugin:", () => {
+        // Arrange
+        const rawExtensionName = "eslint:recommended";
+
+        // Act
+        const extensionNames = resolveExtensionNames(rawExtensionName);
+
+        // Assert
+        expect(extensionNames).toEqual(["eslint:recommended"]);
+    });
 });
diff --git a/src/creation/simplification/retrieveExtendsValues.test.ts b/src/creation/simplification/retrieveExtendsValues.test.ts
index aec6c80..f85cb4b 100644
--- a/src/creation/simplification/retrieveExtendsValues.test.ts
+++ b/src/creation/simplification/retrieveExtendsValues.test.ts
@@ -1,7 +1,7 @@
 import { retrieveExtendsValues } from "./retrieveExtendsValues";
 
 describe("retrieveExtendsValues", () => {
-    it("retrieves eslint-all when an extension is named eslint:all", async () => {
+    it("retrieves an equivalent ESLint configuration when a retrieved extensions is an ESLint builtin", async () => {
         // Arrange
         const eslintAll = { rules: {} };
         const importer = async (extensionName: string) =>
@@ -16,22 +16,22 @@ describe("retrieveExtendsValues", () => {
         expect(importedExtensions).toEqual([eslintAll]);
     });
 
-    it("retrieves eslint-recommended when an extension is named eslint:recommended", async () => {
+    it("retrieves an equivalent typescript-eslint configuration when a retrieved extensions is a typescript-eslint builtin", async () => {
         // Arrange
-        const eslintRecommended = { rules: {} };
+        const eslintAll = { rules: {} };
         const importer = async (extensionName: string) =>
-            extensionName === "eslint/conf/eslint-recommended"
-                ? eslintRecommended
+            extensionName === "node_modules/@typescript-eslint/eslint-plugin/dist/configs/all.json"
+                ? eslintAll
                 : new Error(`Unknown extension name: '${extensionName}`);
 
         // Act
         const { importedExtensions } = await retrieveExtendsValues(
             { importer },
-            "eslint:recommended",
+            "plugin:@typescript-eslint/all",
         );
 
         // Assert
-        expect(importedExtensions).toEqual([eslintRecommended]);
+        expect(importedExtensions).toEqual([eslintAll]);
     });
 
     it("reports a failure when an extension fails to import", async () => {
diff --git a/src/creation/simplification/simplifyPackageRules.test.ts b/src/creation/simplification/simplifyPackageRules.test.ts
index e603dbc..d4fdee0 100644
--- a/src/creation/simplification/simplifyPackageRules.test.ts
+++ b/src/creation/simplification/simplifyPackageRules.test.ts
@@ -5,10 +5,13 @@ import { simplifyPackageRules } from "./simplifyPackageRules";
 
 const createStubDependencies = () => ({
     removeExtendsDuplicatedRules: jest.fn(),
-    retrieveExtendsValues: jest.fn(),
+    retrieveExtendsValues: async () => ({
+        configurationErrors: [],
+        importedExtensions: [],
+    }),
 });
 
-const createStubESLintConfiguration = (fullExtends: string | string[]) => ({
+const createStubESLintConfiguration = (fullExtends: string[]) => ({
     full: {
         env: {},
         extends: fullExtends,
@@ -16,17 +19,24 @@ const createStubESLintConfiguration = (fullExtends: string | string[]) => ({
     },
 });
 
+const createStubTSLintConfiguration = () => ({
+    full: {},
+    raw: {},
+});
+
 describe("simplifyPackageRules", () => {
-    it("returns the conversion results directly when there is no loaded eslint configuration", async () => {
+    it("returns the conversion results directly when there is no loaded ESLint configuration and no TSLint extensions", async () => {
         // Arrange
         const dependencies = createStubDependencies();
         const eslint = undefined;
+        const tslint = createStubTSLintConfiguration();
         const ruleConversionResults = createEmptyConversionResults();
 
         // Act
         const simplifiedResults = await simplifyPackageRules(
             dependencies,
             eslint,
+            tslint,
             ruleConversionResults,
         );
 
@@ -34,16 +44,18 @@ describe("simplifyPackageRules", () => {
         expect(simplifiedResults).toBe(ruleConversionResults);
     });
 
-    it("returns the conversion results directly when the eslint configuration has an empty extends", async () => {
+    it("returns the conversion results directly when there is an empty ESLint configuration and no TSLint extensions", async () => {
         // Arrange
         const dependencies = createStubDependencies();
         const eslint = createStubESLintConfiguration([]);
+        const tslint = createStubTSLintConfiguration();
         const ruleConversionResults = createEmptyConversionResults();
 
         // Act
         const simplifiedResults = await simplifyPackageRules(
             dependencies,
             eslint,
+            tslint,
             ruleConversionResults,
         );
 
@@ -51,7 +63,7 @@ describe("simplifyPackageRules", () => {
         expect(simplifiedResults).toBe(ruleConversionResults);
     });
 
-    it("includes deduplicated rules and extension failures when the eslint configuration extends", async () => {
+    it("includes deduplicated rules and extension failures when the ESLint configuration extends", async () => {
         // Arrange
         const configurationErrors = [new ConfigurationError(new Error("oh no"), "darn")];
         const deduplicatedRules = new Map<string, ESLintRuleOptions>([
@@ -71,19 +83,23 @@ describe("simplifyPackageRules", () => {
                 importedExtensions: [],
             }),
         };
-        const eslint = createStubESLintConfiguration(["extension-name"]);
+        const eslintExtends = ["extension-name"];
+        const eslint = createStubESLintConfiguration(eslintExtends);
+        const tslint = createStubTSLintConfiguration();
         const ruleConversionResults = createEmptyConversionResults();
 
         // Act
         const simplifiedResults = await simplifyPackageRules(
             dependencies,
             eslint,
+            tslint,
             ruleConversionResults,
         );
 
         // Assert
         expect(simplifiedResults).toEqual({
             converted: deduplicatedRules,
+            extends: eslintExtends,
             failed: configurationErrors,
         });
     });
diff --git a/src/creation/writeConversionResults.test.ts b/src/creation/writeConversionResults.test.ts
index b64e1c6..1ada480 100644
--- a/src/creation/writeConversionResults.test.ts
+++ b/src/creation/writeConversionResults.test.ts
@@ -2,8 +2,11 @@ import { createEmptyConversionResults } from "../conversion/conversionResults.st
 import { writeConversionResults } from "./writeConversionResults";
 import { AllOriginalConfigurations } from "../input/findOriginalConfigurations";
 import { formatJsonOutput } from "./formatting/formatters/formatJsonOutput";
+import { SimplifiedRuleConversionResults } from "./simplification/simplifyPackageRules";
 
-const createStubOriginalConfigurations = (overrides: Partial<AllOriginalConfigurations> = {}) => ({
+const createStubOriginalConfigurations = (
+    overrides: Partial<AllOriginalConfigurations & SimplifiedRuleConversionResults> = {},
+) => ({
     tslint: {
         full: {
             rulesDirectory: [],
@@ -146,6 +149,44 @@ describe("writeConversionResults", () => {
         );
     });
 
+    it("includes extensions when they exist", async () => {
+        // Arrange
+        const extension = ["stub-extension"];
+        const conversionResults = {
+            ...createEmptyConversionResults(),
+            extends: extension,
+        };
+        const fileSystem = { writeFile: jest.fn().mockReturnValue(Promise.resolve()) };
+
+        // Act
+        await writeConversionResults(
+            { fileSystem },
+            ".eslintrc.json",
+            conversionResults,
+            createStubOriginalConfigurations(),
+        );
+
+        // Assert
+        expect(fileSystem.writeFile).toHaveBeenLastCalledWith(
+            ".eslintrc.json",
+            formatJsonOutput({
+                env: {
+                    browser: true,
+                    es6: true,
+                    node: true,
+                },
+                extends: extension,
+                parser: "@typescript-eslint/parser",
+                parserOptions: {
+                    project: "tsconfig.json",
+                    sourceType: "module",
+                },
+                plugins: ["@typescript-eslint"],
+                rules: {},
+            }),
+        );
+    });
+
     it("includes raw globals when they exist", async () => {
         // Arrange
         const conversionResults = createEmptyConversionResults();