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

Issue:
Allow configuring the output format of the ESLint configuration file
Right now, it'll always print as `.eslintrc.json`. How about `.js` or `.yaml`?

Pull request description:
Allowed custom .js or .json file outputs with --config
## PR Checklist

-   [x] Addresses an existing issue: fixes #10
-   [x] That issue was marked as [`status: accepting prs`](https://github.com/JoshuaKGoldberg/tslint-to-eslint-config/labels/status%3A%20accepting%20prs)

## Overview

As before, the output path defaults to `--eslintrc.js`. `--eslint` will default to `--config` which has that default.
The format of the output path depends on its file extension, with JavaScript for `.js` files and JSON otherwise.

The README.md list of flags is getting a little long so I added a quick manual table of contents. Will try to remember to split that into a separate PR...

Patch:
diff --git a/README.md b/README.md
index 704a93d..10a3007 100644
--- a/README.md
+++ b/README.md
@@ -36,19 +36,35 @@ TSLint rules without ESLint equivalents will be wrapped with [eslint-plugin-tsli
 
 Each of these flags is optional.
 
+-   **[`config`](#config)**: Path to print the generated ESLint configuration file to.
 -   **[`eslint`](#eslint)**: Path to an ESLint configuration file to read settings from.
 -   **[`package`](#package)**: Path to a package.json file to read dependencies from.
 -   **[`tslint`](#tslint)**: Path to a TSLint configuration file to read settings from.
 -   **[`typescript`](#typescript)**: Path to a TypeScript configuration file to read TypeScript compiler options from.
 
-#### `eslint`
+#### `config`
 
 ```shell
-npx tslint-to-eslint-config --eslint ./path/to/eslintrc.json
+npx tslint-to-eslint-config --config .eslintrc.json
 ```
 
 _Default: `.eslintrc.js`_
 
+Path to print the generated ESLint configuration file to.
+
+The file extension of this path will be used to determine the format of the created file:
+
+-   `.js` file paths will be written `module.exports = ...` JavaScript
+-   Other file paths will default to JSON
+
+#### `eslint`
+
+```shell
+npx tslint-to-eslint-config --eslint ./path/to/eslintrc.js
+```
+
+_Default: `--config`'s value_
+
 Path to an ESLint configuration file to read settings from.
 This isn't yet used for anything, but will eventually inform settings for the generated ESLint configuration file.
 
diff --git a/src/cli/runCli.ts b/src/cli/runCli.ts
index b457e93..b465fbe 100644
--- a/src/cli/runCli.ts
+++ b/src/cli/runCli.ts
@@ -19,13 +19,17 @@ export const runCli = async (
 ): Promise<ResultStatus> => {
     const command = new Command()
         .usage("[options] <file ...> --language [language]")
-        .option("--eslint [eslint]", "eslint configuration file to convert")
-        .option("--package [package]", "package configuration file to convert")
-        .option("--tslint [tslint]", "tslint configuration file to convert")
-        .option("--typescript [typescript]", "typescript configuration file to convert")
+        .option("--config [config]", "eslint configuration file to output to")
+        .option("--eslint [eslint]", "eslint configuration file to convert using")
+        .option("--package [package]", "package configuration file to convert using")
+        .option("--tslint [tslint]", "tslint configuration file to convert using")
+        .option("--typescript [typescript]", "typescript configuration file to convert using")
         .option("-V --version", "output the package version");
 
-    const parsedArgv = command.parse(rawArgv) as Partial<TSLintToESLintSettings>;
+    const parsedArgv = {
+        config: "./eslintrc.js",
+        ...(command.parse(rawArgv) as Partial<TSLintToESLintSettings>),
+    };
 
     if ({}.hasOwnProperty.call(parsedArgv, "version")) {
         dependencies.logger.stdout.write(`${version}${EOL}`);
diff --git a/src/conversion/convertConfig.ts b/src/conversion/convertConfig.ts
index fc4046a..6701fd2 100644
--- a/src/conversion/convertConfig.ts
+++ b/src/conversion/convertConfig.ts
@@ -25,7 +25,11 @@ export const convertConfig = async (
         originalConfigurations.data.tslint.rules,
     );
 
-    await dependencies.writeConversionResults(ruleConversionResults, originalConfigurations.data);
+    await dependencies.writeConversionResults(
+        settings.config,
+        ruleConversionResults,
+        originalConfigurations.data,
+    );
     dependencies.reportConversionResults(ruleConversionResults);
 
     return {
diff --git a/src/creation/formatting/formatOutput.ts b/src/creation/formatting/formatOutput.ts
new file mode 100644
index 0000000..7266791
--- /dev/null
+++ b/src/creation/formatting/formatOutput.ts
@@ -0,0 +1,17 @@
+import { formatJsonOutput } from "./formatters/formatJsonOutput";
+import { formatJsOutput } from "./formatters/formatJsOutput";
+
+const formatters = new Map([["js", formatJsOutput]]);
+
+export const formatOutput = (outputPath: string, configuration: unknown): string => {
+    const customFormatter = formatters.get(getExtension(outputPath));
+    const formatter = customFormatter === undefined ? formatJsonOutput : formatJsOutput;
+
+    return formatter(configuration);
+};
+
+const getExtension = (outputPath: string) => {
+    const periodIndex = outputPath.lastIndexOf(".");
+
+    return periodIndex === -1 ? outputPath : outputPath.slice(periodIndex + 1);
+};
diff --git a/src/creation/formatting/formatters/formatJsOutput.ts b/src/creation/formatting/formatters/formatJsOutput.ts
new file mode 100644
index 0000000..d5e0208
--- /dev/null
+++ b/src/creation/formatting/formatters/formatJsOutput.ts
@@ -0,0 +1,4 @@
+import { EOL } from "os";
+
+export const formatJsOutput = (configuration: unknown) =>
+    `module.exports = ${JSON.stringify(configuration, undefined, 4)};${EOL}`;
diff --git a/src/creation/formatting/formatters/formatJsonOutput.ts b/src/creation/formatting/formatters/formatJsonOutput.ts
new file mode 100644
index 0000000..6aceb00
--- /dev/null
+++ b/src/creation/formatting/formatters/formatJsonOutput.ts
@@ -0,0 +1,4 @@
+import { EOL } from "os";
+
+export const formatJsonOutput = (configuration: unknown) =>
+    `${JSON.stringify(configuration, undefined, 4)}${EOL}`;
diff --git a/src/creation/writeConversionResults.ts b/src/creation/writeConversionResults.ts
index f09fe86..07f6802 100644
--- a/src/creation/writeConversionResults.ts
+++ b/src/creation/writeConversionResults.ts
@@ -3,6 +3,7 @@ import { RuleConversionResults } from "../rules/convertRules";
 import { formatConvertedRules } from "./formatConvertedRules";
 import { OriginalConfigurations } from "../input/findOriginalConfigurations";
 import { createEnv } from "./eslint/createEnv";
+import { formatOutput } from "./formatting/formatOutput";
 
 export type WriteConversionResultsDependencies = {
     fileSystem: Pick<FileSystem, "writeFile">;
@@ -10,6 +11,7 @@ export type WriteConversionResultsDependencies = {
 
 export const writeConversionResults = async (
     dependencies: WriteConversionResultsDependencies,
+    outputPath: string,
     ruleConversionResults: RuleConversionResults,
     originalConfigurations: OriginalConfigurations,
 ) => {
@@ -29,5 +31,5 @@ export const writeConversionResults = async (
         rules: formatConvertedRules(ruleConversionResults, originalConfigurations.tslint),
     };
 
-    await dependencies.fileSystem.writeFile(".eslintrc.json", JSON.stringify(output, undefined, 4));
+    await dependencies.fileSystem.writeFile(outputPath, formatOutput(outputPath, output));
 };
diff --git a/src/input/findESLintConfiguration.ts b/src/input/findESLintConfiguration.ts
index 179c82c..4171dda 100644
--- a/src/input/findESLintConfiguration.ts
+++ b/src/input/findESLintConfiguration.ts
@@ -1,3 +1,4 @@
+import { TSLintToESLintSettings } from "../types";
 import { findConfiguration, FindConfigurationDependencies } from "./findConfiguration";
 
 export type ESLintConfiguration = {
@@ -16,12 +17,12 @@ const defaultESLintConfiguration = {
 
 export const findESLintConfiguration = async (
     dependencies: FindConfigurationDependencies,
-    config: string | undefined,
+    rawSettings: Pick<TSLintToESLintSettings, "config" | "eslint">,
 ): Promise<ESLintConfiguration | Error> => {
     const rawConfiguration = await findConfiguration<ESLintConfiguration>(
         dependencies.exec,
         "eslint --print-config",
-        config || "./eslintrc.js",
+        rawSettings.eslint || rawSettings.config,
     );
 
     return rawConfiguration instanceof Error
diff --git a/src/input/findOriginalConfigurations.ts b/src/input/findOriginalConfigurations.ts
index a6559d9..a3b387b 100644
--- a/src/input/findOriginalConfigurations.ts
+++ b/src/input/findOriginalConfigurations.ts
@@ -27,7 +27,7 @@ export const findOriginalConfigurations = async (
     rawSettings: TSLintToESLintSettings,
 ): Promise<ResultWithDataStatus<OriginalConfigurations>> => {
     const [eslint, packages, tslint, typescript] = await Promise.all([
-        dependencies.findESLintConfiguration(rawSettings.eslint),
+        dependencies.findESLintConfiguration(rawSettings),
         dependencies.findPackagesConfiguration(rawSettings.package),
         dependencies.findTSLintConfiguration(rawSettings.tslint),
         dependencies.findTypeScriptConfiguration(rawSettings.typescript),
diff --git a/src/types.ts b/src/types.ts
index cd34781..474eb3f 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,4 +1,9 @@
 export type TSLintToESLintSettings = {
+    /**
+     * Output ESLint configuration file path, such as `.eslintrc.js`.
+     */
+    config: string;
+
     /**
      * Original ESLint configuration file path, such as `.eslintrc.js`.
      */


Test patch:
diff --git a/src/conversion/convertConfig.test.ts b/src/conversion/convertConfig.test.ts
index 8a8718f..fb21682 100644
--- a/src/conversion/convertConfig.test.ts
+++ b/src/conversion/convertConfig.test.ts
@@ -28,7 +28,9 @@ describe("convertConfig", () => {
         const dependencies = createStubDependencies({
             findOriginalConfigurations: async () => findError,
         });
-        const settings = {};
+        const settings = {
+            config: "./eslintrc.js",
+        };
 
         // Act
         const result = await convertConfig(dependencies, settings);
@@ -46,7 +48,9 @@ describe("convertConfig", () => {
         const dependencies = createStubDependencies({
             findOriginalConfigurations: async () => findSuccess,
         });
-        const settings = {};
+        const settings = {
+            config: "./eslintrc.js",
+        };
 
         // Act
         const result = await convertConfig(dependencies, settings);
diff --git a/src/creation/formatting/formatOutput.test.ts b/src/creation/formatting/formatOutput.test.ts
new file mode 100644
index 0000000..422c161
--- /dev/null
+++ b/src/creation/formatting/formatOutput.test.ts
@@ -0,0 +1,55 @@
+import { EOL } from "os";
+
+import { formatOutput } from "./formatOutput";
+
+describe("formatOutput", () => {
+    it("formats output as JavaScript for a .js file path", () => {
+        // Arrange
+        const outputPath = ".eslintrc.js";
+        const configuration = { rules: {} };
+
+        // Act
+        const output = formatOutput(outputPath, configuration);
+
+        // Assert
+        expect(output).toBe(
+            `module.exports = ${JSON.stringify(configuration, undefined, 4)};${EOL}`,
+        );
+    });
+
+    it("formats output as JSON for a .json file path", () => {
+        // Arrange
+        const outputPath = ".eslintrc.json";
+        const configuration = { rules: {} };
+
+        // Act
+        const output = formatOutput(outputPath, configuration);
+
+        // Assert
+        expect(output).toBe(`${JSON.stringify(configuration, undefined, 4)}${EOL}`);
+    });
+
+    it("formats output as JSON for an unknown dot file path", () => {
+        // Arrange
+        const outputPath = ".eslintrc";
+        const configuration = { rules: {} };
+
+        // Act
+        const output = formatOutput(outputPath, configuration);
+
+        // Assert
+        expect(output).toBe(`${JSON.stringify(configuration, undefined, 4)}${EOL}`);
+    });
+
+    it("formats output as JSON for an unknown raw file path", () => {
+        // Arrange
+        const outputPath = "eslintrc";
+        const configuration = { rules: {} };
+
+        // Act
+        const output = formatOutput(outputPath, configuration);
+
+        // Assert
+        expect(output).toBe(`${JSON.stringify(configuration, undefined, 4)}${EOL}`);
+    });
+});
diff --git a/src/creation/writeConversionResults.test.ts b/src/creation/writeConversionResults.test.ts
index 65258c1..2da4e79 100644
--- a/src/creation/writeConversionResults.test.ts
+++ b/src/creation/writeConversionResults.test.ts
@@ -1,5 +1,6 @@
 import { createEmptyConversionResults } from "../conversion/conversionResults.stubs";
 import { writeConversionResults } from "./writeConversionResults";
+import { formatJsonOutput } from "./formatting/formatters/formatJsonOutput";
 
 const originalConfigurations = {
     tslint: {
@@ -17,29 +18,30 @@ describe("writeConversionResults", () => {
         const fileSystem = { writeFile: jest.fn().mockReturnValue(Promise.resolve()) };
 
         // Act
-        await writeConversionResults({ fileSystem }, conversionResults, originalConfigurations);
+        await writeConversionResults(
+            { fileSystem },
+            ".eslintrc.json",
+            conversionResults,
+            originalConfigurations,
+        );
 
         // Assert
         expect(fileSystem.writeFile).toHaveBeenLastCalledWith(
             ".eslintrc.json",
-            JSON.stringify(
-                {
-                    env: {
-                        browser: true,
-                        es6: true,
-                        node: true,
-                    },
-                    parser: "@typescript-eslint/parser",
-                    parserOptions: {
-                        project: "tsconfig.json",
-                        sourceType: "module",
-                    },
-                    plugins: ["@typescript-eslint"],
-                    rules: {},
+            formatJsonOutput({
+                env: {
+                    browser: true,
+                    es6: true,
+                    node: true,
                 },
-                undefined,
-                4,
-            ),
+                parser: "@typescript-eslint/parser",
+                parserOptions: {
+                    project: "tsconfig.json",
+                    sourceType: "module",
+                },
+                plugins: ["@typescript-eslint"],
+                rules: {},
+            }),
         );
     });
 
@@ -58,38 +60,39 @@ describe("writeConversionResults", () => {
         const fileSystem = { writeFile: jest.fn().mockReturnValue(Promise.resolve()) };
 
         // Act
-        await writeConversionResults({ fileSystem }, conversionResults, originalConfigurations);
+        await writeConversionResults(
+            { fileSystem },
+            ".eslintrc.json",
+            conversionResults,
+            originalConfigurations,
+        );
 
         // Assert
         expect(fileSystem.writeFile).toHaveBeenLastCalledWith(
             ".eslintrc.json",
-            JSON.stringify(
-                {
-                    env: {
-                        browser: true,
-                        es6: true,
-                        node: true,
-                    },
-                    parser: "@typescript-eslint/parser",
-                    parserOptions: {
-                        project: "tsconfig.json",
-                        sourceType: "module",
-                    },
-                    plugins: ["@typescript-eslint", "@typescript-eslint/tslint"],
-                    rules: {
-                        "@typescript-eslint/tslint/config": [
-                            "error",
-                            {
-                                rules: {
-                                    "tslint-rule-one": true,
-                                },
+            formatJsonOutput({
+                env: {
+                    browser: true,
+                    es6: true,
+                    node: true,
+                },
+                parser: "@typescript-eslint/parser",
+                parserOptions: {
+                    project: "tsconfig.json",
+                    sourceType: "module",
+                },
+                plugins: ["@typescript-eslint", "@typescript-eslint/tslint"],
+                rules: {
+                    "@typescript-eslint/tslint/config": [
+                        "error",
+                        {
+                            rules: {
+                                "tslint-rule-one": true,
                             },
-                        ],
-                    },
+                        },
+                    ],
                 },
-                undefined,
-                4,
-            ),
+            }),
         );
     });
 });
diff --git a/src/input/findESLintConfiguration.test.ts b/src/input/findESLintConfiguration.test.ts
index 0615c99..173a40c 100644
--- a/src/input/findESLintConfiguration.test.ts
+++ b/src/input/findESLintConfiguration.test.ts
@@ -1,5 +1,12 @@
 import { findESLintConfiguration } from "./findESLintConfiguration";
 import { createStubExec, createStubThrowingExec } from "../adapters/exec.stubs";
+import { TSLintToESLintSettings } from "../types";
+
+const createStubRawSettings = (overrides: Partial<TSLintToESLintSettings> = {}) => ({
+    config: "./eslintrc.js",
+    eslint: undefined,
+    ...overrides,
+});
 
 describe("findESLintConfiguration", () => {
     it("returns an error when one occurs", async () => {
@@ -8,7 +15,7 @@ describe("findESLintConfiguration", () => {
         const dependencies = { exec: createStubThrowingExec({ stderr: message }) };
 
         // Act
-        const result = await findESLintConfiguration(dependencies, undefined);
+        const result = await findESLintConfiguration(dependencies, createStubRawSettings());
 
         // Assert
         expect(result).toEqual(
@@ -23,7 +30,7 @@ describe("findESLintConfiguration", () => {
         const dependencies = { exec: createStubExec() };
 
         // Act
-        await findESLintConfiguration(dependencies, undefined);
+        await findESLintConfiguration(dependencies, createStubRawSettings());
 
         // Assert
         expect(dependencies.exec).toHaveBeenLastCalledWith("eslint --print-config ./eslintrc.js");
@@ -32,7 +39,9 @@ describe("findESLintConfiguration", () => {
     it("includes a configuration file in the ESLint command when one is provided", async () => {
         // Arrange
         const dependencies = { exec: createStubExec() };
-        const config = "./custom/eslintrc.js";
+        const config = createStubRawSettings({
+            eslint: "./custom/eslintrc.js",
+        });
 
         // Act
         await findESLintConfiguration(dependencies, config);
@@ -46,7 +55,9 @@ describe("findESLintConfiguration", () => {
     it("applies ESLint defaults when none are provided", async () => {
         // Arrange
         const dependencies = { exec: createStubExec({ stdout: "{}" }) };
-        const config = "./custom/eslintrc.js";
+        const config = createStubRawSettings({
+            eslint: "./custom/eslintrc.js",
+        });
 
         // Act
         const result = await findESLintConfiguration(dependencies, config);
diff --git a/src/input/findOriginalConfigurations.test.ts b/src/input/findOriginalConfigurations.test.ts
index c2a36a8..c1777a8 100644
--- a/src/input/findOriginalConfigurations.test.ts
+++ b/src/input/findOriginalConfigurations.test.ts
@@ -5,6 +5,7 @@ import {
 import { ResultStatus } from "../types";
 
 const createRawSettings = () => ({
+    config: "./eslintrc.js",
     eslint: "",
     tslint: "",
     typescript: "",