diff --git a/.npmignore b/.npmignore index 858e84a0..6c5690bc 100644 --- a/.npmignore +++ b/.npmignore @@ -2,6 +2,7 @@ *.log *.map coverage/ +script/ src/**/*.stubs.* src/**/*.test.* src/**/*.ts diff --git a/docs/Creating a Rule Converter.md b/docs/Creating a Rule Converter.md new file mode 100644 index 00000000..5f03e3ff --- /dev/null +++ b/docs/Creating a Rule Converter.md @@ -0,0 +1,16 @@ +# Creating a Rule Converter + +> If you're not familiar with this project's rule converters work, please read the [Architecture docs](./Architecture/README.md) first. + +Adding a new rule converter to `tslint-to-eslint-config` is a relatively straightforward task. +For your convenience, a starter script is included that sets up the files: + +```shell +node ./script/newConverter --eslint output-name --tslint input-name +``` + +If the lint rule includes arguments, add the `--sameArguments` flag above to have starter code generated for that as well. + +```shell +node ./script/newConverter --eslint output-name --tslint input-name --sameArguments +``` diff --git a/docs/Development.md b/docs/Development.md index b9819a7d..9e0e66d7 100644 --- a/docs/Development.md +++ b/docs/Development.md @@ -22,5 +22,6 @@ Compile with `npm run tsc` and run tests with `npm run test`. ## Further Reading - [Architecture](./Architecture/README.md): How the general app structure operates +- [Creating a Rule Converter](./Creating%20a%20Rule%20Converter.md): How to quickly add a missing converter for a TSLint rule - [Dependencies](./Dependencies.md): How functions pass and receive static dependencies - [Testing](./Testing.md): Unit tests diff --git a/script/newConverter/index.js b/script/newConverter/index.js new file mode 100644 index 00000000..ba1f9eb4 --- /dev/null +++ b/script/newConverter/index.js @@ -0,0 +1,31 @@ +const { Command } = require("commander"); +const { upperFirst, camelCase } = require("lodash"); + +const { rewriteConvertersMap } = require("./rewriteConvertersMap"); +const { writeConverter } = require("./writeConverter"); +const { writeConverterTest } = require("./writeConverterTest"); + +(async () => { + const command = new Command() + .option("--eslint [eslint]", "name of the original ESLint rule") + .option("--sameArguments [sameArguments]", "whether to copy over ruleArguments") + .option("--tslint [tslint]", "name of the original TSLint rule"); + + const args = command.parse(process.argv).opts(); + + for (const arg of ["eslint", "tslint"]) { + if (!args[arg]) { + throw new Error(`Missing --${arg} option.`); + } + } + + const tslintPascalCase = upperFirst(camelCase(args.tslint)); + const plugins = args.eslint.includes("/") + ? ` + plugins: ["${args.eslint.split("/")[0]}"],` + : ""; + + await rewriteConvertersMap({ args, tslintPascalCase }); + await writeConverter({ args, plugins, tslintPascalCase }); + await writeConverterTest({ args, plugins, tslintPascalCase }); +})(); diff --git a/script/newConverter/rewriteConvertersMap.js b/script/newConverter/rewriteConvertersMap.js new file mode 100644 index 00000000..28accd7d --- /dev/null +++ b/script/newConverter/rewriteConvertersMap.js @@ -0,0 +1,46 @@ +const { promises: fs } = require("fs"); +const { EOL } = require("node:os"); +const path = require("path"); + +const filePath = "./src/converters/lintConfigs/rules/ruleConverters.ts"; + +module.exports.rewriteConvertersMap = async ({ args, tslintPascalCase }) => { + const lines = (await fs.readFile(filePath)).toString().split(/\r\n|\r|\n/); + + /** + * Inserts a new line alphabetically into the file lines. + * + * @param {string} insertion Line to be added. + * @param {number} start Starting point to begin comparing at. + * @param {number} end Last line to compare at, and add just after as a fallback. + * @param {(line: string) => string} [mapLine] Transforms lines to be sorted. + * @remarks In theory this could use binary search, but... why bother? + */ + const insertAlphabetically = (insertion, start, end, mapLine = (line) => line) => { + const sorter = mapLine(insertion); + + for (let i = start; i < lines.length; i += 1) { + if (mapLine(lines[i]) > sorter) { + lines.splice(i, 0, insertion); + return; + } + } + + lines.splice(end, 0, insertion); + }; + + insertAlphabetically( + `import { convert${tslintPascalCase} } from "./ruleConverters/${args.tslint}";`, + 0, + lines.indexOf(""), + (line) => line.split(" from ")[1], + ); + + insertAlphabetically( + ` ["${args.tslint}", convert${tslintPascalCase}],`, + lines.indexOf("export const ruleConverters = new Map([") + 1, + lines.indexOf("]);"), + ); + + await fs.writeFile(filePath, lines.join(EOL)); +}; diff --git a/script/newConverter/writeConverter.js b/script/newConverter/writeConverter.js new file mode 100644 index 00000000..6c528346 --- /dev/null +++ b/script/newConverter/writeConverter.js @@ -0,0 +1,30 @@ +const { promises: fs } = require("fs"); + +module.exports.writeConverter = async ({ args, plugins, tslintPascalCase }) => { + const [functionArguments, ruleArguments] = args.sameArguments + ? [ + "tslintRule", + ` + ...(tslintRule.ruleArguments.length !== 0 && { + ruleArguments: tslintRule.ruleArguments, + }),`, + ] + : ["", ""]; + + await fs.writeFile( + `./src/converters/lintConfigs/rules/ruleConverters/${args.tslint}.ts`, + ` + import { RuleConverter } from "../ruleConverter"; + +export const convert${tslintPascalCase}: RuleConverter = (${functionArguments}) => { + return {${plugins} + rules: [ + {${ruleArguments} + ruleName: "${args.eslint}", + }, + ], + }; +}; +`.trimLeft(), + ); +}; diff --git a/script/newConverter/writeConverterTest.js b/script/newConverter/writeConverterTest.js new file mode 100644 index 00000000..03906a0c --- /dev/null +++ b/script/newConverter/writeConverterTest.js @@ -0,0 +1,46 @@ +const { promises: fs } = require("fs"); + +module.exports.writeConverterTest = async ({ args, tslintPascalCase, plugins }) => { + const ruleArgumentsTest = args.sameArguments + ? ` + + test("conversion with an argument", () => { + const result = convert${tslintPascalCase}({ + ruleArguments: ["TODO"], + }); + + expect(result).toEqual({${plugins.replace("\n", "\n ")} + rules: [ + { + ruleArguments: ["TODO"], + ruleName: "${args.eslint}", + }, + ], + }); + }); + ` + : ""; + + await fs.writeFile( + `./src/converters/lintConfigs/rules/ruleConverters/tests/${args.tslint}.test.ts`, + ` +import { convert${tslintPascalCase} } from "../${args.tslint}"; + +describe(convert${tslintPascalCase}, () => { + test("conversion without arguments", () => { + const result = convert${tslintPascalCase}({ + ruleArguments: [], + }); + + expect(result).toEqual({${plugins.replace("\n", "\n ")} + rules: [ + { + ruleName: "${args.eslint}", + }, + ], + }); + });${ruleArgumentsTest} +}); +`.trimLeft(), + ); +};