diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..d1ab19785 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,44 @@ +{ + "env": { + "es6": true, + "node": true + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "tsconfig.json", + "sourceType": "module" + }, + "plugins": ["@typescript-eslint", "@typescript-eslint/tslint"], + "rules": { + "@typescript-eslint/array-type": "error", + "@typescript-eslint/interface-name-prefix": "error", + "@typescript-eslint/member-ordering": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-param-reassign": "off", + "@typescript-eslint/no-parameter-properties": "off", + "@typescript-eslint/no-use-before-declare": "off", + "@typescript-eslint/promise-function-async": "off", + "@typescript-eslint/unbound-method": "off", + "arrow-body-style": "off", + "default-case": "off", + "linebreak-style": "off", + "no-bitwise": "off", + "no-empty": "off", + "no-empty-functions": "off", + "no-magic-numbers": "off", + "prefer-template": "off", + "@typescript-eslint/tslint/config": [ + "error", + { + "rules": { + "no-implicit-dependencies": [true, "dev"], + "strict-boolean-expressions": [ + true, + "allow-boolean-or-undefined", + "allow-number" + ] + } + } + ] + } +} diff --git a/.gitignore b/.gitignore index 14a0e6a33..5635acd8f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ coverage/ node_modules/ src/**/*.js +test/*.js +test/tests/**/.eslintrc* diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..7470f987c --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +test/tests/**/* diff --git a/.vscode/settings.json b/.vscode/settings.json index 36eb0f1bc..d695c1f4c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,6 @@ "**/*.js.map": true, "**/*.js": { "when": "$(basename).ts" } }, + "prettier.ignorePath": ".prettierignore", "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/docs/Architecture.md b/docs/Architecture.md new file mode 100644 index 000000000..7116111d1 --- /dev/null +++ b/docs/Architecture.md @@ -0,0 +1,14 @@ +# Architecture + +## CLI Architecture + +- CLI usage starts in `bin/tslint-to-eslint-config`, which immediately calls `src/cli/main.ts`. +- CLI settings are parsed and read in `src/cli/runCli.ts`. +- Application logic is run by `src/conversion/convertConfig.ts`. + +## Dependencies + +Methods are created using a variant of [Dependency Injection](http://en.wikipedia.org/wiki/Dependency_Injection). +Any method with dependencies that should be stubbed out during tests, such as file system bindings, logging, or other methods, +takes in a first argument of name `dependencies`. +Its dependencies object is manually created in `src/cli/main.ts` and bound to the method with `bind`. diff --git a/docs/Development.md b/docs/Development.md index b05e529a8..e35725807 100644 --- a/docs/Development.md +++ b/docs/Development.md @@ -17,32 +17,9 @@ cd tslint-to-eslint-config npm i ``` -Compile with `npm run tsc` and run tests with `npm run test`. +Compile with `npm run tsc` and run tests with `npm run test:unit`. -### CLI Architecture +## More Reading -- CLI usage starts in `bin/tslint-to-eslint-config`, which immediately calls `src/cli/main.ts`. -- CLI settings are parsed and read in `src/cli/runCli.ts`. -- Application logic is run by `src/conversion/convertConfig.ts`. - -### Dependencies - -Methods are created using a variant of [Dependency Injection](http://en.wikipedia.org/wiki/Dependency_Injection). -Any method with dependencies that should be stubbed out during tests, such as file system bindings, logging, or other methods, -takes in a first argument of name `dependencies`. -Its dependencies object is manually created in `src/cli/main.ts` and bound to the method with `bind`. - -### Unit Tests - -Each `src/**/*.ts` source file should have an equivalent `*.test.ts` next to it. -Tests should include comments above each section of the "AAA" testing format: - -- `// Arrange`: Set up dependencies for the code under test -- `// Act`: Perform the logic under test -- `// Assert`: Check the results of the acted logic - -`tslint-to-eslint-config` requires 100% unit test coverage, excluding the following: - -- `src/cli/main.ts` -- `src/adapters/*.ts` -- `src/**/*.stubs.ts` +- [Architecture](./Architecture.md) +- [Testing](./Testing.md) diff --git a/docs/Testing.md b/docs/Testing.md new file mode 100644 index 000000000..0ff7b7d87 --- /dev/null +++ b/docs/Testing.md @@ -0,0 +1,20 @@ +# Testing + +## Unit Tests + +``` +npm run test:unit +``` + +Each `src/**/*.ts` source file should have an equivalent `*.test.ts` next to it. +Tests should include comments above each section of the "AAA" testing format: + +- `// Arrange`: Set up dependencies for the code under test +- `// Act`: Perform the logic under test +- `// Assert`: Check the results of the acted logic + +`tslint-to-eslint-config` requires 100% unit test coverage, excluding the following: + +- `src/cli/main.ts` +- `src/adapters/*.ts` +- `src/**/*.stubs.ts` diff --git a/jest.config.js b/jest.config.js index 1ab81f193..f329ba374 100644 --- a/jest.config.js +++ b/jest.config.js @@ -17,6 +17,6 @@ module.exports = { }, }, moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], - testRegex: "((\\.|/)test)\\.tsx?$", + testRegex: "src(.*)\\.test\\.tsx?$", testEnvironment: "node", }; diff --git a/package.json b/package.json index f858c45d9..6c6dbe9c3 100644 --- a/package.json +++ b/package.json @@ -59,9 +59,11 @@ }, "scripts": { "eslint": "eslint \"./src/*.ts\" \"./src/**/*.ts\" --report-unused-disable-directives", - "prettier": "prettier \"./src/*.{js,json,ts,xml,yaml}\" \"./src/**/*.{js,json,ts,xml,yaml}\"", + "prettier": "prettier \"./src/*.{js,json,ts,xml,yaml}\" \"./src/**/*.{js,json,ts,xml,yaml}\" --ignore-path .prettierignore", "prettier:write": "npm run prettier -- --write", - "test": "jest", + "test:unit": "jest", + "test:end-to-end": "jest --config=test/jest.config.js", + "test:end-to-end:accept": "jest --config=test/jest.config.js --globals=\"{\\\"acceptTestChanges\\\": true }\"", "tsc": "tsc" }, "version": "0.1.7" diff --git a/src/utils.ts b/src/utils.ts index 0d87a3008..8deffdcd4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,3 +5,5 @@ export const isError = (item: Item | Error): item is Error => item instanc export type RemoveErrors = { [P in keyof Items]: Exclude; }; + +export type PromiseValue = T extends Promise ? R : never; diff --git a/test/createTestArgs.ts b/test/createTestArgs.ts new file mode 100644 index 000000000..a4eac8f0a --- /dev/null +++ b/test/createTestArgs.ts @@ -0,0 +1,20 @@ +import * as fs from "fs"; +import * as path from "path"; +import { promisify } from "util"; + +const readdir = promisify(fs.readdir); + +export const createTestArgs = async (cwd: string) => { + const items = new Set(await readdir(cwd)); + const flags = [ + "--config", + path.join(cwd, ".eslintrc.json"), + "--tslint", + path.join(cwd, "tslint.json"), + ]; + + if (items.has("tslint.json")) { + } + + return flags.map(flag => `"${flag}"`).join(" "); +}; diff --git a/test/createTests.ts b/test/createTests.ts new file mode 100644 index 000000000..c7d2b7afc --- /dev/null +++ b/test/createTests.ts @@ -0,0 +1,46 @@ +import * as cp from "child_process"; +import * as fs from "fs"; +import * as path from "path"; +import { promisify } from "util"; + +import { createTestArgs } from "./createTestArgs"; +import { PromiseValue } from "../src/utils"; +import { assertFileContents } from "./expectFileContains"; + +const exec = promisify(cp.exec); +const readFile = promisify(fs.readFile); + +export const createTests = (testName: string, accept: boolean) => { + const cwd = path.join(__dirname, "tests", testName); + const cwdPath = (fileName: string) => path.join(cwd, fileName); + const readTestFile = async (fileName: string) => (await readFile(cwdPath(fileName))).toString(); + + return () => { + let result: PromiseValue>; + beforeAll(async () => { + // Arrange + const args = await createTestArgs(cwd); + + // Act + result = await exec(`ts-node bin/tslint-to-eslint-config ${args}`); + }); + + test("configuration output", async () => { + await assertFileContents( + cwdPath("expected.json"), + await readTestFile(".eslintrc.json"), + accept, + ); + }); + + // test("info log output", () => {}); + + test("stderr", async () => { + await assertFileContents(cwdPath("stderr.txt"), result.stderr, accept); + }); + + test("stdout", async () => { + await assertFileContents(cwdPath("stdout.txt"), result.stdout, accept); + }); + }; +}; diff --git a/test/expectFileContains.ts b/test/expectFileContains.ts new file mode 100644 index 000000000..1add7066d --- /dev/null +++ b/test/expectFileContains.ts @@ -0,0 +1,25 @@ +import * as fs from "fs"; +import { promisify } from "util"; + +const readFile = promisify(fs.readFile); +const writeFile = promisify(fs.writeFile); + +const standardizeEndlines = (text: string) => text.replace(/\r/g, ""); + +export const assertFileContents = async ( + filePath: string, + actual: string | Buffer, + accept: boolean, +) => { + await (accept ? acceptFileContents : expectFileContents)(filePath, actual.toString()); +}; + +const acceptFileContents = async (filePath: string, actual: string) => { + await writeFile(filePath, actual); +}; + +const expectFileContents = async (filePath: string, actual: string) => { + const expected = (await readFile(filePath)).toString(); + + expect(standardizeEndlines(actual)).toEqual(standardizeEndlines(expected)); +}; diff --git a/test/jest.config.js b/test/jest.config.js new file mode 100644 index 000000000..922024be5 --- /dev/null +++ b/test/jest.config.js @@ -0,0 +1,5 @@ +module.exports = { + moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], + testRegex: "test/runEndToEndTests.ts", + testEnvironment: "node", +}; diff --git a/test/runEndToEndTests.ts b/test/runEndToEndTests.ts new file mode 100644 index 000000000..f6349ff48 --- /dev/null +++ b/test/runEndToEndTests.ts @@ -0,0 +1,12 @@ +import * as fs from "fs"; +import * as path from "path"; + +import { createTests } from "./createTests"; + +const testNames = fs.readdirSync(path.join(__dirname, "tests")); + +const accept = "acceptTestChanges" in globalThis; + +for (const testName of testNames) { + describe(testName, createTests(testName, accept)); +} diff --git a/test/tests/standalone tslint.json/.eslintrc.json b/test/tests/standalone tslint.json/.eslintrc.json new file mode 100644 index 000000000..495475d49 --- /dev/null +++ b/test/tests/standalone tslint.json/.eslintrc.json @@ -0,0 +1,50 @@ +{ + "env": { + "es6": true, + "node": true + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "tsconfig.json", + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint", + "@typescript-eslint/tslint" + ], + "rules": { + "@typescript-eslint/array-type": "error", + "@typescript-eslint/interface-name-prefix": "error", + "@typescript-eslint/member-ordering": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-param-reassign": "off", + "@typescript-eslint/no-parameter-properties": "off", + "@typescript-eslint/no-use-before-declare": "off", + "@typescript-eslint/promise-function-async": "off", + "@typescript-eslint/unbound-method": "off", + "arrow-body-style": "off", + "default-case": "off", + "linebreak-style": "off", + "no-bitwise": "off", + "no-empty": "off", + "no-empty-functions": "off", + "no-magic-numbers": "off", + "prefer-template": "off", + "@typescript-eslint/tslint/config": [ + "error", + { + "rules": { + "no-implicit-dependencies": [ + true, + "dev" + ], + "strict-boolean-expressions": [ + true, + "allow-boolean-or-undefined", + "allow-number" + ] + } + } + ] + } +} \ No newline at end of file diff --git a/test/tests/standalone tslint.json/expected.json b/test/tests/standalone tslint.json/expected.json new file mode 100644 index 000000000..495475d49 --- /dev/null +++ b/test/tests/standalone tslint.json/expected.json @@ -0,0 +1,50 @@ +{ + "env": { + "es6": true, + "node": true + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "tsconfig.json", + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint", + "@typescript-eslint/tslint" + ], + "rules": { + "@typescript-eslint/array-type": "error", + "@typescript-eslint/interface-name-prefix": "error", + "@typescript-eslint/member-ordering": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-param-reassign": "off", + "@typescript-eslint/no-parameter-properties": "off", + "@typescript-eslint/no-use-before-declare": "off", + "@typescript-eslint/promise-function-async": "off", + "@typescript-eslint/unbound-method": "off", + "arrow-body-style": "off", + "default-case": "off", + "linebreak-style": "off", + "no-bitwise": "off", + "no-empty": "off", + "no-empty-functions": "off", + "no-magic-numbers": "off", + "prefer-template": "off", + "@typescript-eslint/tslint/config": [ + "error", + { + "rules": { + "no-implicit-dependencies": [ + true, + "dev" + ], + "strict-boolean-expressions": [ + true, + "allow-boolean-or-undefined", + "allow-number" + ] + } + } + ] + } +} \ No newline at end of file diff --git a/test/tests/standalone tslint.json/stderr.txt b/test/tests/standalone tslint.json/stderr.txt new file mode 100644 index 000000000..e69de29bb diff --git a/test/tests/standalone tslint.json/stdout.txt b/test/tests/standalone tslint.json/stdout.txt new file mode 100644 index 000000000..fcf5848f3 --- /dev/null +++ b/test/tests/standalone tslint.json/stdout.txt @@ -0,0 +1,3 @@ +✨ 17 rules replaced with their ESLint equivalents. ✨ +️👀 2 rules do not yet have ESLint equivalents; defaulting to eslint-plugin-tslint. 👀 +✅ All is well! ✅ diff --git a/test/tests/standalone tslint.json/tslint.json b/test/tests/standalone tslint.json/tslint.json new file mode 100644 index 000000000..4a2aa47f4 --- /dev/null +++ b/test/tests/standalone tslint.json/tslint.json @@ -0,0 +1,35 @@ +{ + "rules": { + "array-type": [true, "array"], + "arrow-return-shorthand": false, + "completed-docs": false, + "comment-format": false, + "file-name-casing": false, + "linebreak-style": false, + "interface-name": [true, "never-prefix"], + "member-ordering": false, + "newline-before-return": false, + "no-any": false, + "no-bitwise": false, + "no-empty": false, + "no-magic-numbers": false, + "no-import-side-effect": false, + "no-implicit-dependencies": [true, "dev"], + "no-null-keyword": false, + "no-parameter-reassignment": false, + "no-parameter-properties": false, + "no-submodule-imports": false, + "no-unbound-method": false, + "no-unused-variable": false, + "no-use-before-declare": false, + "prefer-conditional-expression": false, + "prefer-method-signature": false, + "prefer-switch": false, + "prefer-template": false, + "promise-function-async": false, + "strict-boolean-expressions": [true, "allow-boolean-or-undefined", "allow-number"], + "switch-default": false, + "switch-final-break": false, + "typedef": false + } +}