diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..9903b220 Binary files /dev/null and b/.DS_Store differ diff --git a/.eslintignore b/.eslintignore index fc84a655..d96aa3d0 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1 @@ -/src/types/** /lib/** diff --git a/.eslintrc.js b/.eslintrc.js index a609a445..52a8beb7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,11 +10,6 @@ module.exports = { tryExtensions: ['.js', '.json', '.ts', '.d.ts'] } }, - rules: { - 'no-process-exit': 'off', // to investigate if we should throw an error instead of process.exit() - 'node/no-unsupported-features/es-builtins': 'off', - 'node/no-unsupported-features/es-syntax': 'off' - }, overrides: [ { files: ['*.ts'], @@ -24,7 +19,15 @@ module.exports = { ], rules: { '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/no-namespace': 'off' // maybe we should consider enabling it in the future + '@typescript-eslint/no-use-before-define': 'off', + 'node/no-unsupported-features/es-syntax': 'off' + } + }, + { + files: ['*.spec.ts'], + rules: { + '@typescript-eslint/no-var-requires': 'off', + 'node/no-missing-import': 'off' } } ] diff --git a/.github/.DS_Store b/.github/.DS_Store new file mode 100644 index 00000000..17ba774b Binary files /dev/null and b/.github/.DS_Store differ diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1df6f379..b55283f9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,5 +1,5 @@ name: CI/CD -on: [push, pull_request] +on: [pull_request] jobs: build: runs-on: ubuntu-latest @@ -39,21 +39,8 @@ jobs: needs: build strategy: matrix: - os: - - ubuntu-latest - - macos-latest - - windows-latest - node: - - '6' - - '8' - - '10' - - '12' - packages: - - webpack@5.0.0-alpha.5 ts-loader@^5.0.0 vue-loader@^15.2.4 - - webpack@^4.0.0 ts-loader@^5.0.0 vue-loader@^15.2.4 - exclude: - - node: '6' - packages: 'webpack@5.0.0-alpha.5 ts-loader@^5.0.0 vue-loader@^15.2.4' + node: [10, 12] # add 14 when we drop support for webpack 4 as fsevents 1 is not compatible with node 14 + os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@v1 @@ -74,10 +61,7 @@ jobs: ${{ runner.os }}-node-${{ matrix.node }}-yarn- - name: Install dependencies - run: yarn install --frozen-lockfile --ignore-engines - - - name: Replace dependencies - run: yarn add ${{ matrix.packages }} -D --ignore-engines + run: yarn install --frozen-lockfile - name: Download build artifact uses: actions/download-artifact@v1 @@ -86,9 +70,9 @@ jobs: - name: Run unit tests run: yarn test:unit - - - name: Run integration tests - run: yarn test:integration + + - name: Run e2e tests + run: yarn test:e2e release: runs-on: ubuntu-latest @@ -96,7 +80,7 @@ jobs: GH_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} needs: [build, test] - if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/beta') + if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/alpha' || github.ref == 'refs/heads/beta') steps: - uses: actions/checkout@v1 diff --git a/.gitignore b/.gitignore index 99d76db3..e8a005ce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,17 @@ # Logs -logs *.log -npm-debug.log* +# Package artifacts lib -!test/**/lib + +# Package archive used by e2e tests +fork-ts-checker-webpack-plugin.tgz # Coverage directory used by tools like istanbul coverage -# Tmp directory for integration test -tmp - # Dependency directories node_modules -jspm_packages - -# Optional npm cache directory -.npm -package-lock.json - -# Optional REPL history -.node_repl_history # Editor directories and files .idea diff --git a/.nvmrc b/.nvmrc index 7d70dc47..f599e28b 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -6.9.11 +10 diff --git a/.prettierrc b/.prettierrc index adeae874..554f2a3b 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,4 @@ { "singleQuote": true, - "semi": true, - "tabWidth": 2, - "useTabs": false, - "printWidth": 80 + "printWidth": 100 } diff --git a/README.md b/README.md index 5b4d7772..9f922cbf 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@
Webpack plugin that runs TypeScript type checker on a separate process.
[![npm version](https://img.shields.io/npm/v/fork-ts-checker-webpack-plugin.svg)](https://www.npmjs.com/package/fork-ts-checker-webpack-plugin) -[![npm beta version](https://img.shields.io/npm/v/fork-ts-checker-webpack-plugin/beta.svg)](https://www.npmjs.com/package/fork-ts-checker-webpack-plugin) [![build status](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin/workflows/CI/CD/badge.svg?branch=master&event=push)](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin/actions?query=branch%3Amaster+event%3Apush) [![downloads](http://img.shields.io/npm/dm/fork-ts-checker-webpack-plugin.svg)](https://npmjs.org/package/fork-ts-checker-webpack-plugin) [![commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) @@ -13,12 +12,20 @@ -## Installation +## Features + + * Speeds up [TypeScript](https://github.com/Microsoft/TypeScript) type checking and [ESLint](https://eslint.org/) linting (by moving each to a separate process) ๐ + * Supports modern TypeScript features like [project references](https://www.typescriptlang.org/docs/handbook/project-references.html) and [incremental mode](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#faster-subsequent-builds-with-the---incremental-flag) โจ + * Supports [Yarn PnP](https://classic.yarnpkg.com/en/docs/pnp/) ๐งถ + * Supports [Vue Single File Component](https://vuejs.org/v2/guide/single-file-components.html) โ ย + * Displays nice error messages with the [code frame](https://babeljs.io/docs/en/next/babel-code-frame.html) formatter ๐ -This plugin requires minimum **Node.js 6.11.5**, **webpack 4**, **TypeScript 2.1** and optionally **ESLint 6** (which itself requires minimum **Node.js 8.10.0**) +## Installation -If you depend on **webpack 2**, **webpack 3**, or **tslint 4**, please use [older version](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin/tree/v3.1.1) of the plugin. +This plugin requires minimum **Node.js 10**, **Webpack 4**, **TypeScript 2.7** and optionally **ESLint 6** +* If you depend on **Webpack 2**, **Webpack 3**, or **TSLint 4**, please use [version 3](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin/tree/v3.1.1) of the plugin. +* If you depend on **TypeScript >= 2.1** and **< 2.7** or you can't update to **Node 10**, please use [version 4](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin/tree/v4.1.4) of the plugin. ```sh # with npm npm install --save-dev fork-ts-checker-webpack-plugin @@ -27,14 +34,18 @@ npm install --save-dev fork-ts-checker-webpack-plugin yarn add --dev fork-ts-checker-webpack-plugin ``` -Basic webpack config (with [ts-loader](https://github.com/TypeStrong/ts-loader)) +The minimal webpack config (with [ts-loader](https://github.com/TypeStrong/ts-loader)) ```js +// webpack.config.js const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); -const webpackConfig = { +module.exports = { context: __dirname, // to automatically find tsconfig.json entry: './src/index.ts', + resolve: { + extensions: [".ts", ".tsx", ".js"], + }, module: { rules: [ { @@ -51,379 +62,356 @@ const webpackConfig = { }; ``` -## Motivation - -There was already similar solution - [awesome-typescript-loader](https://github.com/s-panferov/awesome-typescript-loader). You can -add `CheckerPlugin` and delegate checker to the separate process. The problem with `awesome-typescript-loader` was that, in our case, -it was a lot slower than [ts-loader](https://github.com/TypeStrong/ts-loader) on an incremental build (~20s vs ~3s). -Secondly, we used [tslint](https://palantir.github.io/tslint) and we wanted to run this, along with type checker, in a separate process. -This is why this plugin was created. To provide better performance, the plugin reuses Abstract Syntax Trees between compilations and shares -these trees with TSLint. +If you are using **TypeScript >= 2.8.0**, it's recommended to set `"importsNotUsedAsValues": "preserve"` [compiler option](https://www.typescriptlang.org/docs/handbook/compiler-options.html) +in the `tsconfig.json`. [Here is an explanation.](#type-only-modules-watching) ## Modules resolution It's very important to be aware that **this plugin uses [TypeScript](https://github.com/Microsoft/TypeScript)'s, not -[webpack](https://github.com/webpack/webpack)'s modules resolution**. It means that you have to setup `tsconfig.json` correctly. For example -if you set `files: ['./src/someFile.ts']` in `tsconfig.json`, this plugin will check only `someFile.ts` for semantic errors. It's because -of performance. The goal of this plugin is to be _as fast as possible_. With TypeScript's module resolution we don't have to wait for webpack -to compile files (which traverses dependency graph during compilation) - we have a full list of files from the begin. +[webpack](https://github.com/webpack/webpack)'s modules resolution**. It means that you have to setup `tsconfig.json` correctly. +For example if you set `files: ['./src/index.ts']` in `tsconfig.json`, this plugin will check only `index.ts` for errors. -To debug TypeScript's modules resolution, you can use `tsc --traceResolution` command. +> It's because of the performance - with TypeScript's module resolution we don't have to wait for webpack to compile files. +> +> To debug TypeScript's modules resolution, you can use `tsc --traceResolution` command. ## ESLint -[ESLint is the future of linting in the TypeScript world.](https://eslint.org/blog/2019/01/future-typescript-eslint) If you'd like to use eslint with the plugin, supply this option: `eslint: true` and ensure you have the relevant dependencies installed: +If you'd like to use ESLint with the plugin, ensure you have the relevant dependencies installed: + +```sh +# with npm +npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin -`yarn add eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --dev` +# with yarn +yarn add --dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin +``` + +Then set up ESLint in the plugin. This is the minimal configuration: +```js +// webpack.config.js +const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); + +module.exports = { + // ...the webpack configuration + plugins: [ + new ForkTsCheckerWebpackPlugin({ + eslint: { + files: './src/**/*.ts' // required - same as command `eslint ./src/**/*.ts` + } + }) + ] +}; +``` -You should have an ESLint configuration file in your root project directory. Here is a sample `.eslintrc.js` configuration for a TypeScript project: +You should also have an ESLint configuration file in your root project directory. +Here is a sample `.eslintrc.js` configuration for a TypeScript project: ```js const path = require('path'); + module.exports = { - parser: '@typescript-eslint/parser', // Specifies the ESLint parser + parser: '@typescript-eslint/parser', // specifies the ESLint parser extends: [ - 'plugin:@typescript-eslint/recommended' // Uses the recommended rules from the @typescript-eslint/eslint-plugin + 'plugin:@typescript-eslint/recommended' // uses the recommended rules from the @typescript-eslint/eslint-plugin ], parserOptions: { project: path.resolve(__dirname, './tsconfig.json'), tsconfigRootDir: __dirname, - ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features - sourceType: 'module', // Allows for the use of imports + ecmaVersion: 2018, // allows for the parsing of modern ECMAScript features + sourceType: 'module', // allows for the use of imports }, rules: { - // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs + // place to specify ESLint rules - can be used to overwrite rules specified from the extended configs // e.g. "@typescript-eslint/explicit-function-return-type": "off", } }; ``` -There's a good explanation on setting up TypeScript ESLint support by Robert Cooper [here](https://dev.to/robertcoopercode/using-eslint-and-prettier-in-a-typescript-project-53jb). +There's a [good explanation on setting up TypeScript ESLint support by Robert Cooper](https://dev.to/robertcoopercode/using-eslint-and-prettier-in-a-typescript-project-53jb). ## Options -- **tsconfig** `string`: - Path to _tsconfig.json_ file. Default: `path.resolve(compiler.options.context, './tsconfig.json')`. - -- **compilerOptions** `object`: - Allows overriding TypeScript options. Should be specified in the same format as you would do for the `compilerOptions` property in tsconfig.json. Default: `{}`. - -- **eslint** `true | undefined`: - - - If `true`, this activates eslint support. - -- **eslintOptions** `object`: - - - Options that can be used to initialise ESLint. See https://eslint.org/docs/developer-guide/nodejs-api#cliengine +This plugin uses [`cosmiconfig`](https://github.com/davidtheclark/cosmiconfig). This means that besides the plugin constructor, +you can place your configuration in the: + * `"fork-ts-checker"` field in the `package.json` + * `.fork-ts-checkerrc` file in JSON or YAML format + * `fork-ts-checker.config.js` file exporting a JS object -- **async** `boolean`: - True by default - `async: false` can block webpack's emit to wait for type checker/linter and to add errors to the webpack's compilation. - We recommend to set this to `false` in projects where type checking is faster than webpack's build - it's better for integration with other plugins. Another scenario where you might want to set this to `false` is if you use the `overlay` functionality of `webpack-dev-server`. +Options passed to the plugin constructor will overwrite options from the cosmiconfig (using [deepmerge](https://github.com/TehShrike/deepmerge)). -- **ignoreDiagnostics** `number[]`: - List of TypeScript diagnostic codes to ignore. +| Name | Type | Default value | Description | +| ----------------- | --------------------- | ------------------------------------------------ | ----------- | +| `async` | `boolean` | `compiler.options.mode === 'development'` | If `true`, reports issues **after** webpack's compilation is done. Thanks to that it doesn't block the compilation. Used only in the `watch` mode. | +| `typescript` | `object` or `boolean` | `true` | If a `boolean`, it enables/disables TypeScript checker. If an `object`, see [TypeScript options](#typescript-options). | +| `eslint` | `object` | `undefined` | If `undefined`, it disables ESLint linter. If an `object`, see [ESLint options](#eslint-options). | +| `issue` | `object` | `{}` | See [Issues options](#issues-options). | +| `formatter` | `string` or `object` | `codeframe` | Available formatters are `basic` and `codeframe`. To [configure](https://babeljs.io/docs/en/babel-code-frame#options) `codeframe` formatter, pass object: `{ type: 'codeframe', options: {+ Hello {userName}! + +
+ + + + + + +/// src/model/User.ts +import Role from './Role'; + +type User = { + id: string; + email: string; + role: Role; + firstName?: string; + lastName?: string; +} + +function getUserName(user: User): string { + return [user.firstName, user.lastName] + .filter(name => name !== undefined) + .join(' '); +} + +export default User; +export { getUserName }; + +/// src/model/Role.ts +type Role = "admin" | "client" | "provider"; + +export default Role; diff --git a/test/e2e/fixtures/type-definitions.fixture b/test/e2e/fixtures/type-definitions.fixture new file mode 100644 index 00000000..3913d6a5 --- /dev/null +++ b/test/e2e/fixtures/type-definitions.fixture @@ -0,0 +1,46 @@ +/// package.json +{ + "name": "type-definitions-fixture", + "version": "1.0.0", + "main": "dist/index.js", + "license": "MIT", + "scripts": { + "tsc": "tsc" + }, + "devDependencies": { + "fork-ts-checker-webpack-plugin": ${FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION}, + "typescript": "~3.8.0" + } +} + +/// tsconfig.json +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "lib": ["ES6", "DOM"], + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "strict": true, + "baseUrl": "./" + }, + "include": ["./"], + "exclude": ["node_modules"] +} + +/// webpack.config.ts +import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; + +const config = { + entry: './src/index.ts', + plugins: [ + new ForkTsCheckerWebpackPlugin({ + async: 'invalid_value', + typescript: { + enabled: true + } + }) + ] +}; diff --git a/test/e2e/sandbox/Fixture.ts b/test/e2e/sandbox/Fixture.ts new file mode 100644 index 00000000..1c543279 --- /dev/null +++ b/test/e2e/sandbox/Fixture.ts @@ -0,0 +1,67 @@ +import fs from 'fs-extra'; + +type FixtureFilePath = string; +type FixtureFileContent = string; + +type Fixture = RecordHello
', - '', - '', - '' - ].join('\n'); - - const result = VueProgram.resolveScriptBlock(ts, content, templateCompiler); - expect(result.content).toBe( - [ - '//', - '//', - '//', - '//', - '', - 'import Vue from "vue";', - 'export default Vue.extend({});', - '' - ].join('\n') - ); - }); - - describe('loadProgramConfig', () => { - it('sets allowNonTsExtensions to true on returned options', () => { - const result = VueProgram.loadProgramConfig( - // eslint-disable-next-line @typescript-eslint/no-var-requires - require('typescript'), - 'tsconfig.foo.json', - {} - ); - - expect(result.options.allowNonTsExtensions).toBe(true); - }); - - it('merges compilerOptions into config file options', () => { - // eslint-disable-next-line @typescript-eslint/no-var-requires - VueProgram.loadProgramConfig(require('typescript'), 'tsconfig.foo.json', { - bar: false - }); - - expect(ts.parseJsonConfigFileContent).toHaveBeenLastCalledWith( - { - compilerOptions: { - allowNonTsExtensions: true, - foo: true, - bar: false - } - }, - expect.anything(), - expect.anything() - ); - }); - }); -}); diff --git a/test/unit/issue/eslint/EsLintIssueFactory.spec.ts b/test/unit/eslint-reporter/issue/EsLintIssueFactory.spec.ts similarity index 50% rename from test/unit/issue/eslint/EsLintIssueFactory.spec.ts rename to test/unit/eslint-reporter/issue/EsLintIssueFactory.spec.ts index 346ee803..8cf50d39 100644 --- a/test/unit/issue/eslint/EsLintIssueFactory.spec.ts +++ b/test/unit/eslint-reporter/issue/EsLintIssueFactory.spec.ts @@ -1,11 +1,7 @@ -import { createIssuesFromEsLintReports } from '../../../../lib/issue'; -import { - LintMessage, - LintReport, - LintResult -} from '../../../../lib/types/eslint'; +import { LintMessage, LintResult } from '../../../../lib/types/eslint'; +import { createIssuesFromEsLintResults } from '../../../../lib/eslint-reporter/issue/EsLintIssueFactory'; -describe('[UNIT] issue/eslint/EsLintIssueFactory', () => { +describe('eslint-reporter/issue/EsLintIssueFactory', () => { const ES_LINT_MESSAGE_ERROR: LintMessage = { column: 0, line: 13, @@ -15,7 +11,7 @@ describe('[UNIT] issue/eslint/EsLintIssueFactory', () => { message: `'y' is assigned a value but never used.`, nodeType: '', severity: 0, - source: null + source: null, }; const ES_LINT_MESSAGE_WARNING: LintMessage = { column: 10, @@ -24,7 +20,7 @@ describe('[UNIT] issue/eslint/EsLintIssueFactory', () => { message: `'y' is assigned a value but never used.`, nodeType: '', severity: 1, - source: null + source: null, }; const ES_LINT_RESULT_INDEX: LintResult = { filePath: 'src/index.ts', @@ -32,7 +28,7 @@ describe('[UNIT] issue/eslint/EsLintIssueFactory', () => { errorCount: 1, warningCount: 0, fixableErrorCount: 0, - fixableWarningCount: 0 + fixableWarningCount: 0, }; const ES_LINT_RESULT_ANOTHER: LintResult = { filePath: 'src/another.ts', @@ -40,7 +36,7 @@ describe('[UNIT] issue/eslint/EsLintIssueFactory', () => { errorCount: 0, warningCount: 1, fixableErrorCount: 0, - fixableWarningCount: 0 + fixableWarningCount: 0, }; const ES_LINT_RESULT_ADDITIONAL: LintResult = { filePath: 'src/additional.ts', @@ -48,30 +44,13 @@ describe('[UNIT] issue/eslint/EsLintIssueFactory', () => { errorCount: 1, warningCount: 0, fixableErrorCount: 0, - fixableWarningCount: 0 + fixableWarningCount: 0, }; - const ES_LINT_REPORT_A: LintReport = { - results: [ES_LINT_RESULT_INDEX, ES_LINT_RESULT_ANOTHER], - errorCount: 1, - warningCount: 1, - fixableErrorCount: 0, - fixableWarningCount: 0 - }; - const ES_LINT_REPORT_B: LintReport = { - results: [ES_LINT_RESULT_ADDITIONAL], - errorCount: 1, - warningCount: 1, - fixableErrorCount: 0, - fixableWarningCount: 0 - }; - const ES_LINT_REPORTS = [ES_LINT_REPORT_A, ES_LINT_REPORT_B]; + const ES_LINT_RESULTS = [ES_LINT_RESULT_INDEX, ES_LINT_RESULT_ANOTHER, ES_LINT_RESULT_ADDITIONAL]; - it.each([[ES_LINT_REPORTS]])( - 'creates Issues from EsLint Reports: %p', - reports => { - const issues = createIssuesFromEsLintReports(reports); + it.each([[ES_LINT_RESULTS]])('creates Issues from EsLint Results: %p', (results) => { + const issues = createIssuesFromEsLintResults(results); - expect(issues).toMatchSnapshot(); - } - ); + expect(issues).toMatchSnapshot(); + }); }); diff --git a/test/unit/eslint-reporter/issue/__snapshots__/EsLintIssueFactory.spec.ts.snap b/test/unit/eslint-reporter/issue/__snapshots__/EsLintIssueFactory.spec.ts.snap new file mode 100644 index 00000000..65b45e85 --- /dev/null +++ b/test/unit/eslint-reporter/issue/__snapshots__/EsLintIssueFactory.spec.ts.snap @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`eslint-reporter/issue/EsLintIssueFactory creates Issues from EsLint Results: [[Object], [Object], [Object]] 1`] = ` +Array [ + Object { + "code": "no-unused-vars", + "file": "src/index.ts", + "location": Object { + "end": Object { + "column": 5, + "line": 13, + }, + "start": Object { + "column": 0, + "line": 13, + }, + }, + "message": "'y' is assigned a value but never used.", + "origin": "eslint", + "severity": "error", + }, + Object { + "code": "no-unused-vars", + "file": "src/another.ts", + "location": Object { + "end": Object { + "column": 10, + "line": 15, + }, + "start": Object { + "column": 10, + "line": 15, + }, + }, + "message": "'y' is assigned a value but never used.", + "origin": "eslint", + "severity": "warning", + }, + Object { + "code": "no-unused-vars", + "file": "src/additional.ts", + "location": Object { + "end": Object { + "column": 5, + "line": 13, + }, + "start": Object { + "column": 0, + "line": 13, + }, + }, + "message": "'y' is assigned a value but never used.", + "origin": "eslint", + "severity": "error", + }, +] +`; diff --git a/test/unit/formatter/BasicFormatter.spec.ts b/test/unit/formatter/BasicFormatter.spec.ts new file mode 100644 index 00000000..65826c3f --- /dev/null +++ b/test/unit/formatter/BasicFormatter.spec.ts @@ -0,0 +1,53 @@ +import { Issue } from 'lib/issue'; +import { createBasicFormatter } from 'lib/formatter'; + +describe('formatter/BasicFormatter', () => { + it.each([ + [ + { + origin: 'typescript', + severity: 'error', + code: 'TS2322', + message: `Type '"1"' is not assignable to type 'number'.`, + file: 'src/index.ts', + location: { + start: { + line: 10, + column: 4, + }, + end: { + line: 10, + column: 6, + }, + }, + }, + `TS2322: Type '"1"' is not assignable to type 'number'.`, + ], + [ + { + origin: 'eslint', + severity: 'warning', + code: 'no-unused-vars', + message: `'x' is assigned a value but never used.`, + file: 'src/index.ts', + location: { + start: { + line: 12, + column: 8, + }, + end: { + line: 12, + column: 9, + }, + }, + }, + `no-unused-vars: 'x' is assigned a value but never used.`, + ], + ])('formats issue message "%p" to "%p"', (...args) => { + const [issue, expectedFormatted] = args as [Issue, string]; + const formatter = createBasicFormatter(); + const formatted = formatter(issue); + + expect(formatted).toEqual(expectedFormatted); + }); +}); diff --git a/test/unit/formatter/CodeframeFormatter.spec.ts b/test/unit/formatter/CodeframeFormatter.spec.ts index 1616fafb..f9a59bd5 100644 --- a/test/unit/formatter/CodeframeFormatter.spec.ts +++ b/test/unit/formatter/CodeframeFormatter.spec.ts @@ -1,21 +1,21 @@ import * as os from 'os'; import mockFs from 'mock-fs'; -import { Issue, IssueOrigin, IssueSeverity } from '../../../lib/issue'; -import { createCodeframeFormatter } from '../../../lib/formatter'; +import { Issue } from 'lib/issue'; +import { createCodeframeFormatter } from 'lib/formatter'; -describe('[UNIT] formatter/CodeframeFormatter', () => { +describe('formatter/CodeframeFormatter', () => { beforeEach(() => { mockFs({ src: { 'index.ts': [ - 'const y: number = "1";', - 'const x = 1;', + 'const foo: number = "1";', + 'const bar = 1;', '', - 'function z() {', - ' console.log(y);', - '}' - ].join('\n') - } + 'function baz() {', + ' console.log(baz);', + '}', + ].join('\n'), + }, }); }); @@ -26,70 +26,111 @@ describe('[UNIT] formatter/CodeframeFormatter', () => { it.each([ [ { - origin: IssueOrigin.TYPESCRIPT, - severity: IssueSeverity.ERROR, - code: '2322', + origin: 'typescript', + severity: 'error', + code: 'TS2322', message: `Type '"1"' is not assignable to type 'number'.`, file: 'src/index.ts', - line: 1, - character: 7 + location: { + start: { + line: 1, + column: 7, + }, + end: { + line: 1, + column: 10, + }, + }, }, [ - `ERROR in src/index.ts(1,7):`, - `1:7 Type '"1"' is not assignable to type 'number'.`, - ' > 1 | const y: number = "1";', - ' | ^', - ' 2 | const x = 1;' - ].join(os.EOL) + `TS2322: Type '"1"' is not assignable to type 'number'.`, + ' > 1 | const foo: number = "1";', + ' | ^^^', + ' 2 | const bar = 1;', + ].join(os.EOL), ], [ { - origin: IssueOrigin.TYPESCRIPT, - severity: IssueSeverity.ERROR, - code: '2322', + origin: 'typescript', + severity: 'error', + code: 'TS2322', message: `Type '"1"' is not assignable to type 'number'.`, - file: 'src/not-existing.ts', - line: 1, - character: 7 + file: 'src/index.ts', }, - [ - `ERROR in src/not-existing.ts(1,7):`, - `1:7 Type '"1"' is not assignable to type 'number'.` - ].join(os.EOL) + `TS2322: Type '"1"' is not assignable to type 'number'.`, ], [ { - origin: IssueOrigin.ESLINT, - severity: IssueSeverity.WARNING, - code: 'no-unused-vars', - message: `'x' is assigned a value but never used.`, + origin: 'typescript', + severity: 'error', + code: 'TS2322', + message: `Type '"1"' is not assignable to type 'number'.`, file: 'src/index.ts', - line: 2, - character: 7 + location: { + start: { + line: 1, + column: 7, + }, + }, }, [ - `WARNING in src/index.ts(2,7):`, - `2:7 'x' is assigned a value but never used.`, - ' 1 | const y: number = "1";', - ' > 2 | const x = 1;', + `TS2322: Type '"1"' is not assignable to type 'number'.`, + ' > 1 | const foo: number = "1";', ' | ^', - ' 3 | ' - ].join(os.EOL) + ' 2 | const bar = 1;', + ].join(os.EOL), ], [ { - origin: IssueOrigin.INTERNAL, - severity: IssueSeverity.ERROR, - code: 'INTERNAL', - message: `Stack overflow - out of memory` + origin: 'typescript', + severity: 'error', + code: 'TS2322', + message: `Type '"1"' is not assignable to type 'number'.`, + file: 'src/not-existing.ts', + location: { + start: { + line: 1, + column: 7, + }, + end: { + line: 1, + column: 10, + }, + }, }, - 'INTERNAL ERROR: Stack overflow - out of memory' - ] + `TS2322: Type '"1"' is not assignable to type 'number'.`, + ], + [ + { + origin: 'eslint', + severity: 'warning', + code: 'no-unused-vars', + message: `'bar' is assigned a value but never used.`, + file: 'src/index.ts', + location: { + start: { + line: 2, + column: 7, + }, + end: { + line: 2, + column: 10, + }, + }, + }, + [ + `no-unused-vars: 'bar' is assigned a value but never used.`, + ' 1 | const foo: number = "1";', + ' > 2 | const bar = 1;', + ' | ^^^', + ' 3 | ', + ].join(os.EOL), + ], ])('formats issue message "%p" to "%p"', (...args) => { const [issue, expectedFormatted] = args as [Issue, string]; const formatter = createCodeframeFormatter({ linesAbove: 1, - linesBelow: 1 + linesBelow: 1, }); const formatted = formatter(issue); diff --git a/test/unit/formatter/DefaultFormatter.spec.ts b/test/unit/formatter/DefaultFormatter.spec.ts deleted file mode 100644 index 813d60d4..00000000 --- a/test/unit/formatter/DefaultFormatter.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import * as os from 'os'; -import { Issue, IssueOrigin, IssueSeverity } from '../../../lib/issue'; -import { createDefaultFormatter } from '../../../lib/formatter'; - -describe('[UNIT] formatter/DefaultFormatter', () => { - it.each([ - [ - { - origin: IssueOrigin.TYPESCRIPT, - severity: IssueSeverity.ERROR, - code: '2322', - message: `Type '"1"' is not assignable to type 'number'.`, - file: 'src/index.ts', - line: 1, - character: 7 - }, - [ - `ERROR in src/index.ts(1,7):`, - `TS2322: Type '"1"' is not assignable to type 'number'.` - ].join(os.EOL) - ], - [ - { - origin: IssueOrigin.ESLINT, - severity: IssueSeverity.WARNING, - code: 'no-unused-vars', - message: `'x' is assigned a value but never used.`, - file: 'src/index.ts', - line: 2, - character: 10 - }, - [ - `WARNING in src/index.ts(2,10):`, - `no-unused-vars: 'x' is assigned a value but never used.` - ].join(os.EOL) - ], - [ - { - origin: IssueOrigin.INTERNAL, - severity: IssueSeverity.ERROR, - code: 'INTERNAL', - message: `Stack overflow - out of memory` - }, - 'INTERNAL ERROR: Stack overflow - out of memory' - ] - ])('formats issue message "%p" to "%p"', (...args) => { - const [issue, expectedFormatted] = args as [Issue, string]; - const formatter = createDefaultFormatter(); - const formatted = formatter(issue); - - expect(formatted).toEqual(expectedFormatted); - }); -}); diff --git a/test/unit/formatter/FormatterConfiguration.spec.ts b/test/unit/formatter/FormatterConfiguration.spec.ts new file mode 100644 index 00000000..6ab4199d --- /dev/null +++ b/test/unit/formatter/FormatterConfiguration.spec.ts @@ -0,0 +1,72 @@ +import os from 'os'; +import mockFs from 'mock-fs'; +import { Issue } from 'lib/issue'; +import { createFormatterConfiguration, FormatterOptions } from 'lib/formatter'; + +describe('formatter/FormatterConfiguration', () => { + beforeEach(() => { + mockFs({ + src: { + 'index.ts': [ + 'const foo: number = "1";', + 'const bar = 1;', + '', + 'function baz() {', + ' console.log(baz);', + '}', + ].join('\n'), + }, + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + const issue: Issue = { + origin: 'typescript', + severity: 'error', + code: 'TS2322', + message: `Type '"1"' is not assignable to type 'number'.`, + file: 'src/index.ts', + location: { + start: { + line: 1, + column: 7, + }, + end: { + line: 1, + column: 10, + }, + }, + }; + + const BASIC_FORMATTER_OUTPUT = `TS2322: Type '"1"' is not assignable to type 'number'.`; + const CODEFRAME_FORMATTER_OUTPUT = [ + BASIC_FORMATTER_OUTPUT, + ' > 1 | const foo: number = "1";', + ' | ^^^', + ' 2 | const bar = 1;', + ' 3 | ', + ' 4 | function baz() {', + ].join(os.EOL); + const CUSTOM_CODEFRAME_FORMATTER_OUTPUT = [ + BASIC_FORMATTER_OUTPUT, + ' > 1 | const foo: number = "1";', + ' | ^^^', + ' 2 | const bar = 1;', + ].join(os.EOL); + + it.each([ + [undefined, CODEFRAME_FORMATTER_OUTPUT], + ['basic', BASIC_FORMATTER_OUTPUT], + ['codeframe', CODEFRAME_FORMATTER_OUTPUT], + [{ type: 'basic' }, BASIC_FORMATTER_OUTPUT], + [{ type: 'codeframe' }, CODEFRAME_FORMATTER_OUTPUT], + [{ type: 'codeframe', options: { linesBelow: 1 } }, CUSTOM_CODEFRAME_FORMATTER_OUTPUT], + ])('creates configuration from options', (options, expectedFormat) => { + const formatter = createFormatterConfiguration(options as FormatterOptions); + + expect(formatter(issue)).toEqual(expectedFormat); + }); +}); diff --git a/test/unit/formatter/FormatterFactory.spec.ts b/test/unit/formatter/FormatterFactory.spec.ts index 2758592d..b8491dfc 100644 --- a/test/unit/formatter/FormatterFactory.spec.ts +++ b/test/unit/formatter/FormatterFactory.spec.ts @@ -1,9 +1,9 @@ import * as os from 'os'; import mockFs from 'mock-fs'; -import { createFormatter, FormatterType } from '../../../lib/formatter'; -import { Issue, IssueOrigin, IssueSeverity } from '../../../lib/issue'; +import { Issue } from 'lib/issue'; +import { createFormatter, FormatterType } from 'lib/formatter'; -describe('[UNIT] formatter/FormatterFactory', () => { +describe('formatter/FormatterFactory', () => { beforeEach(() => { mockFs({ some: { @@ -13,9 +13,9 @@ describe('[UNIT] formatter/FormatterFactory', () => { ' constructor() {', " console.log('anything special');", ' }', - '}' - ].join('\n') - } + '}', + ].join('\n'), + }, }); }); @@ -24,22 +24,28 @@ describe('[UNIT] formatter/FormatterFactory', () => { }); const issue: Issue = { - origin: IssueOrigin.TYPESCRIPT, - severity: IssueSeverity.ERROR, - code: '123', + origin: 'typescript', + severity: 'error', + code: 'TS123', message: 'Some issue content', file: 'some/file.ts', - line: 1, - character: 7 + location: { + start: { + line: 1, + column: 7, + }, + end: { + line: 1, + column: 16, + }, + }, }; - it.each(['default', undefined])('creates default formatter', type => { + it.each(['basic', undefined])('creates basic formatter', (type) => { const formatter = createFormatter(type as FormatterType); const formattedMessage = formatter(issue); - expect(formattedMessage).toEqual( - ['ERROR in some/file.ts(1,7):', 'TS123: Some issue content'].join(os.EOL) - ); + expect(formattedMessage).toEqual('TS123: Some issue content'); }); it('creates codeframe formatter', () => { @@ -48,13 +54,12 @@ describe('[UNIT] formatter/FormatterFactory', () => { expect(formattedMessage).toEqual( [ - 'ERROR in some/file.ts(1,7):', - '1:7 Some issue content', + 'TS123: Some issue content', ' > 1 | class SomeClass {', - ' | ^', + ' | ^^^^^^^^^', ' 2 | private someProperty: boolean;', ' 3 | constructor() {', - " 4 | console.log('anything special');" + " 4 | console.log('anything special');", ].join(os.EOL) ); }); @@ -62,32 +67,24 @@ describe('[UNIT] formatter/FormatterFactory', () => { it('creates codeframe formatter with custom options', () => { const formatter = createFormatter('codeframe', { linesAbove: 1, - linesBelow: 1 + linesBelow: 1, }); const formattedMessage = formatter(issue); expect(formattedMessage).toEqual( [ - 'ERROR in some/file.ts(1,7):', - '1:7 Some issue content', + 'TS123: Some issue content', ' > 1 | class SomeClass {', - ' | ^', - ' 2 | private someProperty: boolean;' + ' | ^^^^^^^^^', + ' 2 | private someProperty: boolean;', ].join(os.EOL) ); }); - it('forwards already created formatter', () => { - const formatter = createFormatter(issue => issue.message); - const formattedMessage = formatter(issue); - - expect(formattedMessage).toEqual('Some issue content'); - }); - it('throws an error on unknown formatter type', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any expect(() => createFormatter('unknown-type' as any)).toThrowError( - `Unknown "unknown-type" formatter. Available types are: default, codeframe.` + `Unknown "unknown-type" formatter. Available types are: basic, codeframe.` ); }); }); diff --git a/test/unit/formatter/InternalFormatter.spec.ts b/test/unit/formatter/InternalFormatter.spec.ts deleted file mode 100644 index 8fd8a56d..00000000 --- a/test/unit/formatter/InternalFormatter.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import * as os from 'os'; -import { Issue, IssueSeverity, IssueOrigin } from '../../../lib/issue'; -import { createInternalFormatter } from '../../../lib/formatter'; - -describe('[UNIT] formatter/InternalFormatter', () => { - it.each([ - [ - { - origin: IssueOrigin.INTERNAL, - severity: IssueSeverity.ERROR, - code: 'INTERNAL', - message: `Stack overflow - out of memory` - }, - 'INTERNAL ERROR: Stack overflow - out of memory' - ], - [ - { - origin: IssueOrigin.INTERNAL, - severity: IssueSeverity.ERROR, - code: 'INTERNAL', - message: `Stack overflow - out of memory`, - stack: [ - `Security context: 0x35903c44a49