diff --git a/.gitignore b/.gitignore index b34ba9fd7..0b68bcb25 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.tsbuildinfo *.d.ts +!src/typings/**/*.d.ts *.log *.map coverage/ diff --git a/README.md b/README.md index a50d7d035..f88739e01 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ Each of these flags is optional: - **[`editor`](#editor)**: Path to an editor configuration file to convert linter settings within. - **[`eslint`](#eslint)**: Path to an ESLint configuration file to read settings from. - **[`package`](#package)**: Path to a package.json file to read dependencies from. +- **[`prettier`](#prettier)**: Add `eslint-config-prettier` to the plugins list. - **[`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. @@ -97,6 +98,22 @@ _Default: `package.json`_ Path to a `package.json` file to read dependencies from. This will help inform the generated ESLint configuration file's [env](https://eslint.org/docs/user-guide/configuring#specifying-parser-options) settings. +#### `prettier` + +```shell +npx tslint-to-eslint-config --prettier +``` + +_Default: `false`_ + +Add [`eslint-config-prettier`](https://github.com/prettier/eslint-config-prettier) to the list of ESLint plugins. +We [highly recommend](./docs/FAQs.md#should-i-use-prettier) you use [Prettier](http://prettier.io) for code formatting. + +When `--prettier` isn't enabled: + +- If the output configuration already doesn't enable any formatting rules, it'll extend from `eslint-config-prettier`. +- Otherwise, a CLI message will suggest running with `--prettier`. + #### `tslint` ```shell diff --git a/docs/Architecture.md b/docs/Architecture.md index ec2224a31..d7c10dc4a 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -14,6 +14,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 and TSLint presets + - 3a. If no output rules conflict with `eslint-config-prettier`, it's added in 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-lock.json b/package-lock.json index 7bcbbd52b..6f6046d28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4558,7 +4558,6 @@ "version": "6.11.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz", "integrity": "sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA==", - "dev": true, "requires": { "get-stdin": "^6.0.0" } @@ -5057,8 +5056,7 @@ "get-stdin": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", - "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", - "dev": true + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==" }, "get-stream": { "version": "4.1.0", diff --git a/package.json b/package.json index 95033d71b..63ac3985e 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "chalk": "4.0.0", "commander": "5.1.0", + "eslint-config-prettier": "^6.11.0", "strip-json-comments": "3.1.0", "tslint": "6.1.1", "typescript": "3.8.3" @@ -28,7 +29,6 @@ "@typescript-eslint/parser": "2.29.0", "babel-jest": "25.4.0", "eslint": "6.8.0", - "eslint-config-prettier": "6.11.0", "husky": "4.2.5", "jest": "25.4.0", "lint-staged": "10.1.7", diff --git a/src/cli/main.ts b/src/cli/main.ts index 37d8ef9f8..bc2883176 100644 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -10,6 +10,7 @@ import { convertEditorConfig, ConvertEditorConfigDependencies, } from "../conversion/convertEditorConfig"; +import { addPrettierExtensions } from "../creation/simplification/prettier/addPrettierExtensions"; import { removeExtendsDuplicatedRules } from "../creation/simplification/removeExtendsDuplicatedRules"; import { retrieveExtendsValues, @@ -103,6 +104,7 @@ const retrieveExtendsValuesDependencies: RetrieveExtendsValuesDependencies = { }; const simplifyPackageRulesDependencies: SimplifyPackageRulesDependencies = { + addPrettierExtensions, removeExtendsDuplicatedRules, retrieveExtendsValues: bind(retrieveExtendsValues, retrieveExtendsValuesDependencies), }; diff --git a/src/cli/runCli.ts b/src/cli/runCli.ts index a1a353861..92223d4fb 100644 --- a/src/cli/runCli.ts +++ b/src/cli/runCli.ts @@ -20,11 +20,12 @@ export const runCli = async ( const command = new Command() .usage("[options] --language [language]") .option("--config [config]", "eslint configuration file to output to") + .option("--editor [editor]", "editor configuration file to convert") .option("--eslint [eslint]", "eslint configuration file to convert using") .option("--package [package]", "package configuration file to convert using") + .option("--prettier [prettier]", "add eslint-config-prettier to the plugins list") .option("--tslint [tslint]", "tslint configuration file to convert using") .option("--typescript [typescript]", "typescript configuration file to convert using") - .option("--editor [editor]", "editor configuration file to convert") .option("-V --version", "output the package version"); const parsedArgv = { diff --git a/src/conversion/conversionResults.stubs.ts b/src/conversion/conversionResults.stubs.ts index b3a6ffdfc..fbcc05dd8 100644 --- a/src/conversion/conversionResults.stubs.ts +++ b/src/conversion/conversionResults.stubs.ts @@ -1,9 +1,9 @@ import { EditorSettingConversionResults } from "../editorSettings/convertEditorSettings"; -import { RuleConversionResults } from "../rules/convertRules"; +import { SimplifiedResultsConfiguration } from "../creation/simplification/simplifyPackageRules"; export const createEmptyConversionResults = ( - overrides: Partial = {}, -): RuleConversionResults => ({ + overrides: Partial = {}, +): SimplifiedResultsConfiguration => ({ converted: new Map(), failed: [], missing: [], diff --git a/src/conversion/convertConfig.test.ts b/src/conversion/convertConfig.test.ts index 66f202de1..c3f4c8b4f 100644 --- a/src/conversion/convertConfig.test.ts +++ b/src/conversion/convertConfig.test.ts @@ -17,7 +17,10 @@ const createStubDependencies = ( simplifyPackageRules: async (_configurations, data) => ({ ...data, converted: new Map(), + extends: [], failed: [], + missing: [], + plugins: new Set(), }), writeConversionResults: jest.fn().mockReturnValue(Promise.resolve()), ...overrides, diff --git a/src/conversion/convertConfig.ts b/src/conversion/convertConfig.ts index 67e8b93d1..d7bbae2f3 100644 --- a/src/conversion/convertConfig.ts +++ b/src/conversion/convertConfig.ts @@ -34,14 +34,12 @@ export const convertConfig = async ( ); // 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, - )), - }; + const simplifiedConfiguration = await dependencies.simplifyPackageRules( + originalConfigurations.data.eslint, + originalConfigurations.data.tslint, + ruleConversionResults, + settings.prettier, + ); // 4. The simplified configuration is written to the output config file const fileWriteError = await dependencies.writeConversionResults( diff --git a/src/creation/simplification/prettier/addPrettierExtensions.test.ts b/src/creation/simplification/prettier/addPrettierExtensions.test.ts new file mode 100644 index 000000000..2f21ece5b --- /dev/null +++ b/src/creation/simplification/prettier/addPrettierExtensions.test.ts @@ -0,0 +1,59 @@ +import { createEmptyConversionResults } from "../../../conversion/conversionResults.stubs"; +import { addPrettierExtensions } from "./addPrettierExtensions"; + +const createStubRuleConversions = (ruleName: string, ruleSeverity: "error" | "off") => + new Map([[ruleName, { ruleName, ruleSeverity }]]); + +describe("addPrettierExtensions", () => { + it("returns false when a matching converted rule is enabled", async () => { + // Arrange + const ruleConversionResults = createEmptyConversionResults({ + converted: createStubRuleConversions("max-len", "error"), + }); + + // Act + const result = await addPrettierExtensions(ruleConversionResults); + + // Assert + expect(result).toEqual(false); + }); + + it("returns true when a matching converted rule is disabled", async () => { + // Arrange + const ruleConversionResults = createEmptyConversionResults({ + converted: createStubRuleConversions("max-len", "off"), + }); + + // Act + const result = await addPrettierExtensions(ruleConversionResults); + + // Assert + expect(result).toEqual(true); + }); + + it("returns true when there are no matching converted rules", async () => { + // Arrange + const ruleConversionResults = createEmptyConversionResults({ + converted: createStubRuleConversions("unknown", "error"), + }); + + // Act + const result = await addPrettierExtensions(ruleConversionResults); + + // Assert + expect(result).toEqual(true); + }); + + it("returns true when prettier is requested", async () => { + // Arrange + const ruleConversionResults = createEmptyConversionResults({ + converted: createStubRuleConversions("max-len", "off"), + }); + + // Act + const result = await addPrettierExtensions(ruleConversionResults, true); + + // Assert + expect(result).toEqual(true); + }); +}); diff --git a/src/creation/simplification/prettier/addPrettierExtensions.ts b/src/creation/simplification/prettier/addPrettierExtensions.ts new file mode 100644 index 000000000..703cb6f85 --- /dev/null +++ b/src/creation/simplification/prettier/addPrettierExtensions.ts @@ -0,0 +1,21 @@ +import prettierRuleSettings from "eslint-config-prettier"; + +import { RuleConversionResults } from "../../../rules/convertRules"; + +export const addPrettierExtensions = async ( + ruleConversionResults: Pick, + prettierRequested?: boolean, +) => { + if (prettierRequested) { + return true; + } + + for (const rule in prettierRuleSettings.rules) { + const convertedRule = ruleConversionResults.converted.get(rule); + if (convertedRule !== undefined && convertedRule.ruleSeverity !== "off") { + return false; + } + } + + return true; +}; diff --git a/src/creation/simplification/simplifyPackageRules.test.ts b/src/creation/simplification/simplifyPackageRules.test.ts index d4fdee016..ea119d452 100644 --- a/src/creation/simplification/simplifyPackageRules.test.ts +++ b/src/creation/simplification/simplifyPackageRules.test.ts @@ -1,14 +1,16 @@ import { ConfigurationError } from "../../errors/configurationError"; import { ESLintRuleOptions } from "../../rules/types"; import { createEmptyConversionResults } from "../../conversion/conversionResults.stubs"; -import { simplifyPackageRules } from "./simplifyPackageRules"; +import { simplifyPackageRules, SimplifyPackageRulesDependencies } from "./simplifyPackageRules"; -const createStubDependencies = () => ({ +const createStubDependencies = (overrides: Partial = {}) => ({ + addPrettierExtensions: jest.fn(), removeExtendsDuplicatedRules: jest.fn(), retrieveExtendsValues: async () => ({ configurationErrors: [], importedExtensions: [], }), + ...overrides, }); const createStubESLintConfiguration = (fullExtends: string[]) => ({ @@ -25,7 +27,7 @@ const createStubTSLintConfiguration = () => ({ }); describe("simplifyPackageRules", () => { - it("returns the conversion results directly when there is no loaded ESLint configuration and no TSLint extensions", async () => { + it("returns equivalent conversion results when there is no loaded ESLint configuration and no TSLint extensions", async () => { // Arrange const dependencies = createStubDependencies(); const eslint = undefined; @@ -41,10 +43,36 @@ describe("simplifyPackageRules", () => { ); // Assert - expect(simplifiedResults).toBe(ruleConversionResults); + expect(simplifiedResults).toEqual(ruleConversionResults); }); - it("returns the conversion results directly when there is an empty ESLint configuration and no TSLint extensions", async () => { + it("adds Prettier extensions when addPrettierExtensions returns true", async () => { + // Arrange + const dependencies = createStubDependencies({ + addPrettierExtensions: async () => true, + }); + const eslint = undefined; + const tslint = createStubTSLintConfiguration(); + const ruleConversionResults = createEmptyConversionResults(); + + // Act + const simplifiedResults = await simplifyPackageRules( + dependencies, + eslint, + tslint, + ruleConversionResults, + true, + ); + + // Assert + expect(simplifiedResults).toEqual({ + ...ruleConversionResults, + converted: undefined, + extends: ["eslint-config-prettier", "eslint-config-prettier/@typescript-eslint"], + }); + }); + + it("returns equivalent conversion results when there is an empty ESLint configuration and no TSLint extensions", async () => { // Arrange const dependencies = createStubDependencies(); const eslint = createStubESLintConfiguration([]); @@ -60,7 +88,7 @@ describe("simplifyPackageRules", () => { ); // Assert - expect(simplifiedResults).toBe(ruleConversionResults); + expect(simplifiedResults).toEqual(ruleConversionResults); }); it("includes deduplicated rules and extension failures when the ESLint configuration extends", async () => { @@ -76,13 +104,13 @@ describe("simplifyPackageRules", () => { }, ], ]); - const dependencies = { + const dependencies = createStubDependencies({ removeExtendsDuplicatedRules: () => deduplicatedRules, retrieveExtendsValues: async () => ({ configurationErrors, importedExtensions: [], }), - }; + }); const eslintExtends = ["extension-name"]; const eslint = createStubESLintConfiguration(eslintExtends); const tslint = createStubTSLintConfiguration(); @@ -98,8 +126,9 @@ describe("simplifyPackageRules", () => { // Assert expect(simplifiedResults).toEqual({ + ...ruleConversionResults, converted: deduplicatedRules, - extends: eslintExtends, + extends: [...eslintExtends], failed: configurationErrors, }); }); diff --git a/src/creation/simplification/simplifyPackageRules.ts b/src/creation/simplification/simplifyPackageRules.ts index b7b9d8bd8..e16f18b71 100644 --- a/src/creation/simplification/simplifyPackageRules.ts +++ b/src/creation/simplification/simplifyPackageRules.ts @@ -5,18 +5,17 @@ import { TSLintConfiguration } from "../../input/findTSLintConfiguration"; import { RuleConversionResults } from "../../rules/convertRules"; import { uniqueFromSources } from "../../utils"; import { collectTSLintRulesets } from "./collectTSLintRulesets"; +import { addPrettierExtensions } from "./prettier/addPrettierExtensions"; import { removeExtendsDuplicatedRules } from "./removeExtendsDuplicatedRules"; import { retrieveExtendsValues } from "./retrieveExtendsValues"; export type SimplifyPackageRulesDependencies = { + addPrettierExtensions: typeof addPrettierExtensions; removeExtendsDuplicatedRules: typeof removeExtendsDuplicatedRules; retrieveExtendsValues: SansDependencies; }; -export type SimplifiedRuleConversionResults = Pick< - RuleConversionResults, - "converted" | "failed" -> & { +export type SimplifiedResultsConfiguration = RuleConversionResults & { extends?: string[]; }; @@ -28,11 +27,18 @@ export const simplifyPackageRules = async ( dependencies: SimplifyPackageRulesDependencies, eslint: Pick, "full"> | undefined, tslint: OriginalConfigurations>, - ruleConversionResults: SimplifiedRuleConversionResults, -): Promise => { + ruleConversionResults: RuleConversionResults, + prettierRequested?: boolean, +): Promise => { const extendedESLintRulesets = eslint?.full.extends ?? []; const extendedTSLintRulesets = collectTSLintRulesets(tslint); const allExtensions = uniqueFromSources(extendedESLintRulesets, extendedTSLintRulesets); + + // 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"); + } + if (allExtensions.length === 0) { return ruleConversionResults; } @@ -47,6 +53,7 @@ export const simplifyPackageRules = async ( ); return { + ...ruleConversionResults, converted, extends: allExtensions, failed: [...ruleConversionResults.failed, ...configurationErrors], diff --git a/src/creation/writeConversionResults.test.ts b/src/creation/writeConversionResults.test.ts index 1ada4801d..95c66ff7f 100644 --- a/src/creation/writeConversionResults.test.ts +++ b/src/creation/writeConversionResults.test.ts @@ -1,11 +1,11 @@ import { createEmptyConversionResults } from "../conversion/conversionResults.stubs"; -import { writeConversionResults } from "./writeConversionResults"; import { AllOriginalConfigurations } from "../input/findOriginalConfigurations"; import { formatJsonOutput } from "./formatting/formatters/formatJsonOutput"; -import { SimplifiedRuleConversionResults } from "./simplification/simplifyPackageRules"; +import { SimplifiedResultsConfiguration } from "./simplification/simplifyPackageRules"; +import { writeConversionResults } from "./writeConversionResults"; const createStubOriginalConfigurations = ( - overrides: Partial = {}, + overrides: Partial = {}, ) => ({ tslint: { full: { diff --git a/src/creation/writeConversionResults.ts b/src/creation/writeConversionResults.ts index de39fb2ca..cc379215b 100644 --- a/src/creation/writeConversionResults.ts +++ b/src/creation/writeConversionResults.ts @@ -1,10 +1,9 @@ import { FileSystem } from "../adapters/fileSystem"; -import { RuleConversionResults } from "../rules/convertRules"; import { AllOriginalConfigurations } from "../input/findOriginalConfigurations"; import { createEnv } from "./eslint/createEnv"; import { formatConvertedRules } from "./formatConvertedRules"; import { formatOutput } from "./formatting/formatOutput"; -import { SimplifiedRuleConversionResults } from "./simplification/simplifyPackageRules"; +import { SimplifiedResultsConfiguration } from "./simplification/simplifyPackageRules"; export type WriteConversionResultsDependencies = { fileSystem: Pick; @@ -13,7 +12,7 @@ export type WriteConversionResultsDependencies = { export const writeConversionResults = async ( dependencies: WriteConversionResultsDependencies, outputPath: string, - ruleConversionResults: RuleConversionResults & SimplifiedRuleConversionResults, + ruleConversionResults: SimplifiedResultsConfiguration, originalConfigurations: AllOriginalConfigurations, ) => { const plugins = ["@typescript-eslint"]; diff --git a/src/input/findESLintConfiguration.ts b/src/input/findESLintConfiguration.ts index 0b26dc7c1..f066520cf 100644 --- a/src/input/findESLintConfiguration.ts +++ b/src/input/findESLintConfiguration.ts @@ -39,9 +39,9 @@ export type FindESLintConfigurationDependencies = { export const findESLintConfiguration = async ( dependencies: FindESLintConfigurationDependencies, - rawSettings: Pick, + config: Pick, ): Promise | Error> => { - const filePath = rawSettings.eslint ?? rawSettings.config; + const filePath = config.eslint ?? config.config; const [rawConfiguration, reportedConfiguration] = await Promise.all([ findRawConfiguration(dependencies.importer, filePath, { extends: [], @@ -67,7 +67,7 @@ export const findESLintConfiguration = async ( full: { ...defaultESLintConfiguration, ...reportedConfiguration, - extends: Array.from(new Set(extensions)), + extends: extensions, }, raw: rawConfiguration, }; diff --git a/src/reporting/reportConversionResults.test.ts b/src/reporting/reportConversionResults.test.ts index fb9525074..75ca32429 100644 --- a/src/reporting/reportConversionResults.test.ts +++ b/src/reporting/reportConversionResults.test.ts @@ -13,6 +13,8 @@ const createStubDependencies = (packageManager = PackageManager.Yarn) => { return { choosePackageManager, logger }; }; +const basicExtends = ["eslint-config-prettier", "eslint-config-prettier/@typescript-eslint"]; + describe("reportConversionResults", () => { it("logs a successful conversion without notices when there is one converted rule without notices", async () => { // Arrange @@ -27,6 +29,7 @@ describe("reportConversionResults", () => { }, ], ]), + extends: basicExtends, }); const { choosePackageManager, logger } = createStubDependencies(); @@ -43,8 +46,8 @@ describe("reportConversionResults", () => { logger.stdout.write, `✨ 1 rule replaced with its ESLint equivalent. ✨`, ``, - `⚡ 3 packages are required for running with ESLint. ⚡`, - ` yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint --dev`, + `⚡ 4 packages are required for this ESLint configuration. ⚡`, + ` yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-prettier --dev`, ); }); @@ -62,6 +65,7 @@ describe("reportConversionResults", () => { }, ], ]), + extends: basicExtends, }); const { choosePackageManager, logger } = createStubDependencies(); @@ -80,8 +84,8 @@ describe("reportConversionResults", () => { `❗ 1 ESLint rule behaves differently from its TSLint counterpart ❗`, ` Check ${logger.debugFileName} for details.`, ``, - `⚡ 3 packages are required for running with ESLint. ⚡`, - ` yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint --dev`, + `⚡ 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, @@ -115,6 +119,7 @@ describe("reportConversionResults", () => { }, ], ]), + extends: basicExtends, }); const { choosePackageManager, logger } = createStubDependencies(); @@ -134,8 +139,8 @@ describe("reportConversionResults", () => { `❗ 2 ESLint rules behave differently from their TSLint counterparts ❗`, ` Check ${logger.debugFileName} for details.`, ``, - `⚡ 3 packages are required for running with ESLint. ⚡`, - ` yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint --dev`, + `⚡ 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, @@ -153,6 +158,7 @@ describe("reportConversionResults", () => { // Arrange const conversionResults = createEmptyConversionResults({ failed: [{ getSummary: () => "It broke." }], + extends: basicExtends, }); const { choosePackageManager, logger } = createStubDependencies(); @@ -175,6 +181,7 @@ describe("reportConversionResults", () => { it("logs failed conversions when there are multiple failed conversions", async () => { // Arrange const conversionResults = createEmptyConversionResults({ + extends: basicExtends, failed: [{ getSummary: () => "It broke." }, { getSummary: () => "It really broke." }], }); @@ -198,6 +205,7 @@ describe("reportConversionResults", () => { it("logs a missing rule when there is a missing rule", async () => { // Arrange const conversionResults = createEmptyConversionResults({ + extends: basicExtends, missing: [ { ruleArguments: ["a", "b"], @@ -223,8 +231,8 @@ describe("reportConversionResults", () => { ` 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.`, ``, - `⚡ 4 packages are required for running with ESLint. ⚡`, - ` yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser @typescript-eslint/tslint-eslint-plugin eslint --dev`, + `⚡ 5 packages are required for this ESLint configuration. ⚡`, + ` yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser @typescript-eslint/tslint-eslint-plugin eslint eslint-config-prettier --dev`, ); expectEqualWrites( logger.info.write, @@ -236,6 +244,7 @@ describe("reportConversionResults", () => { it("logs missing rules when there are missing rules", async () => { // Arrange const conversionResults = createEmptyConversionResults({ + extends: basicExtends, missing: [ { ruleArguments: ["a", "b"], @@ -266,8 +275,8 @@ describe("reportConversionResults", () => { ` 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.`, ``, - `⚡ 4 packages are required for running with ESLint. ⚡`, - ` yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser @typescript-eslint/tslint-eslint-plugin eslint --dev`, + `⚡ 5 packages are required for this ESLint configuration. ⚡`, + ` yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser @typescript-eslint/tslint-eslint-plugin eslint eslint-config-prettier --dev`, ); expectEqualWrites( logger.info.write, @@ -280,6 +289,7 @@ describe("reportConversionResults", () => { it("logs missing plugins when there are missing plugins", async () => { // Arrange const conversionResults = createEmptyConversionResults({ + extends: basicExtends, plugins: new Set(["plugin-one", "plugin-two"]), }); @@ -295,8 +305,35 @@ describe("reportConversionResults", () => { // Assert expectEqualWrites( logger.stdout.write, - `⚡ 5 packages are required for running with ESLint. ⚡`, - ` yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint plugin-one plugin-two --dev`, + `⚡ 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`, + ); + }); + + it("logs a Prettier recommendation when eslint-config-prettier isn't extended", async () => { + // Arrange + const conversionResults = createEmptyConversionResults({ + extends: [], + }); + + const { choosePackageManager, logger } = createStubDependencies(); + + // Act + await reportConversionResults( + { choosePackageManager, 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/reportConversionResults.ts b/src/reporting/reportConversionResults.ts index 76c4782b9..30f1c3aa7 100644 --- a/src/reporting/reportConversionResults.ts +++ b/src/reporting/reportConversionResults.ts @@ -3,7 +3,7 @@ import { EOL } from "os"; import { Logger } from "../adapters/logger"; import { SansDependencies } from "../binding"; -import { RuleConversionResults } from "../rules/convertRules"; +import { SimplifiedResultsConfiguration } from "../creation/simplification/simplifyPackageRules"; import { ESLintRuleOptions, TSLintRuleOptions } from "../rules/types"; import { choosePackageManager } from "./packages/choosePackageManager"; import { @@ -21,7 +21,7 @@ export type ReportConversionResultsDependencies = { export const reportConversionResults = async ( dependencies: ReportConversionResultsDependencies, outputPath: string, - ruleConversionResults: RuleConversionResults, + ruleConversionResults: SimplifiedResultsConfiguration, ) => { const packageManager = await dependencies.choosePackageManager(); @@ -49,6 +49,10 @@ export const reportConversionResults = async ( } logMissingPackages(ruleConversionResults, packageManager, dependencies.logger); + + if (!ruleConversionResults.extends?.includes("eslint-config-prettier")) { + logPrettierExtension(dependencies.logger); + } }; type RuleWithNotices = { @@ -70,9 +74,9 @@ const logNotices = (converted: Map, logger: Logger) = ? " behaves differently from its TSLint counterpart" : "s behave differently from their TSLint counterparts"; - logger.stdout.write( - chalk.blueBright(`${EOL}❗ ${rulesWithNotices.length} ESLint rule${behavior} ❗${EOL}`), - ); + logger.stdout.write(chalk.blueBright(`${EOL}❗ ${rulesWithNotices.length}`)); + logger.stdout.write(chalk.blue(` ESLint rule${behavior} `)); + logger.stdout.write(chalk.blueBright(`❗${EOL}`)); logger.stdout.write(chalk.blue(` Check ${logger.debugFileName} for details.${EOL}`)); logger.info.write(`${rulesWithNotices.length} ESLint rule${behavior}:${EOL}`); @@ -86,3 +90,17 @@ const logNotices = (converted: Map, logger: Logger) = logger.info.write(EOL); }; + +const logPrettierExtension = (logger: Logger) => { + logger.stdout.write(chalk.redBright(`${EOL}☠ Prettier`)); + logger.stdout.write(chalk.red(` plugins are missing from your configuration. `)); + logger.stdout.write(chalk.redBright(`☠${EOL}`)); + logger.stdout.write(chalk.red(` We highly recommend running `)); + logger.stdout.write(chalk.redBright(`tslint-to-eslint-config --prettier`)); + logger.stdout.write(chalk.red(` to disable formatting ESLint rules.${EOL}`)); + logger.stdout.write( + chalk.red( + ` See https://github/typescript-eslint/tslint-to-eslint-config/docs/FAQs.md#should-i-use-prettier.${EOL}`, + ), + ); +}; diff --git a/src/reporting/reportOutputs.test.ts b/src/reporting/reportOutputs.test.ts index 04464bb5a..4e7f01ce5 100644 --- a/src/reporting/reportOutputs.test.ts +++ b/src/reporting/reportOutputs.test.ts @@ -2,11 +2,15 @@ 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) => ({ +const createStubDependencies = ( + packageManager: PackageManager, + results?: Partial, +) => ({ logger: createStubLogger(), packageManager, - ruleConversionResults: createEmptyConversionResults(), + ruleConversionResults: createEmptyConversionResults(results), }); describe("reportOutputs", () => { @@ -23,7 +27,7 @@ describe("reportOutputs", () => { // Assert expectEqualWrites( logger.stdout.write, - `⚡ 3 packages are required for running with ESLint. ⚡`, + `⚡ 3 packages are required for this ESLint configuration. ⚡`, ` npm install @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint --save-dev`, ); }); @@ -40,7 +44,7 @@ describe("reportOutputs", () => { // Assert expectEqualWrites( logger.stdout.write, - `⚡ 3 packages are required for running with ESLint. ⚡`, + `⚡ 3 packages are required for this ESLint configuration. ⚡`, ` pnpm add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint --save-dev`, ); }); @@ -57,8 +61,28 @@ describe("reportOutputs", () => { // Assert expectEqualWrites( logger.stdout.write, - `⚡ 3 packages are required for running with ESLint. ⚡`, + `⚡ 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`, + ); + }); }); diff --git a/src/reporting/reportOutputs.ts b/src/reporting/reportOutputs.ts index e400eabac..a925cd1aa 100644 --- a/src/reporting/reportOutputs.ts +++ b/src/reporting/reportOutputs.ts @@ -2,11 +2,12 @@ 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"; -import { RuleConversionResults } from "../rules/convertRules"; export type EditorSettingEntry = Pick; @@ -70,22 +71,25 @@ export const logMissingConversionTarget = ( }; export const logMissingPackages = ( - ruleConversionResults: Pick, + ruleConversionResults: Pick, packageManager: PackageManager, logger: Logger, ) => { - const packageNames = [ + const packageNames = uniqueFromSources([ "@typescript-eslint/eslint-plugin", "@typescript-eslint/parser", ruleConversionResults.missing.length !== 0 && "@typescript-eslint/tslint-eslint-plugin", "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 running with ESLint.")); + 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(" ")))); diff --git a/src/types.ts b/src/types.ts index b4d7f7cc4..8a3d7ec12 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,6 +4,11 @@ export type TSLintToESLintSettings = { */ config: string; + /** + * Original Editor configuration file path, such as `.vscode/settings.json`. + */ + editor?: string; + /** * Original ESLint configuration file path, such as `.eslintrc.js`. */ @@ -14,6 +19,11 @@ export type TSLintToESLintSettings = { */ package?: string; + /** + * Add `eslint-config-prettier` to the plugins list. + */ + prettier?: boolean; + /** * Original TSLint configuration file path, such as `tslint.json`. */ @@ -23,11 +33,6 @@ export type TSLintToESLintSettings = { * Original TypeScript configuration file path, such as `tsconfig.json`. */ typescript?: string; - - /** - * Original Editor configuration file path, such as `.vscode/settings.json`. - */ - editor?: string; }; export type TSLintToESLintResult = ResultWithStatus; diff --git a/src/typings/eslint-config-prettier.d.ts b/src/typings/eslint-config-prettier.d.ts new file mode 100644 index 000000000..69c1d231b --- /dev/null +++ b/src/typings/eslint-config-prettier.d.ts @@ -0,0 +1,4 @@ +// Pending https://github.com/DefinitelyTyped/DefinitelyTyped/pull/44245... +declare module "eslint-config-prettier" { + export const rules: Record; +}