diff --git a/.gitignore b/.gitignore index 4fc7112f..b0adcaf5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ lib # Package archive used by e2e tests -fork-ts-checker-webpack-plugin.tgz +fork-ts-checker-webpack-plugin-0.0.0-semantic-release.tgz # Coverage directory used by tools like istanbul coverage diff --git a/README.md b/README.md index 0c6517ca..41a1fd5a 100644 --- a/README.md +++ b/README.md @@ -153,16 +153,17 @@ Options passed to the plugin constructor will overwrite options from the cosmico Options for the TypeScript checker (`typescript` option object). -| Name | Type | Default value | Description | -| -------------------- | --------------------- | ------------------------------------------------------------------------- | ----------- | -| `enabled` | `boolean` | `true` | If `true`, it enables TypeScript checker. | -| `memoryLimit` | `number` | `2048` | Memory limit for the checker process in MB. If the process exits with the allocation failed error, try to increase this number. | -| `tsconfig` | `string` | `'tsconfig.json'` | Path to the `tsconfig.json` file (path relative to the `compiler.options.context` or absolute path) | -| `build` | `boolean` | `false` | If truthy, it's the equivalent of the `--build` flag for the `tsc` command. | -| `mode` | `'readonly'` or `'write-tsbuildinfo'` or `'write-references'` | `'readonly'` | If you use the `babel-loader`, it's recommended to use `write-references` mode to improve initial compilation time. If you use `ts-loader`, it's recommended to use `readonly` mode to not overwrite filed emitted by `ts-loader`. | -| `compilerOptions` | `object` | `{ skipLibCheck: true, skipDefaultLibCheck: true }` | These options will overwrite compiler options from the `tsconfig.json` file. | -| `diagnosticsOptions` | `object` | `{ syntactic: false, semantic: true, declaration: false, global: false }` | Settings to select which diagnostics do we want to perform. | -| `extensions` | `object` | `{}` | See [TypeScript extensions options](#typescript-extensions-options). | +| Name | Type | Default value | Description | +| -------------------- | --------- | ------------------------------------------------------------------------- | ----------- | +| `enabled` | `boolean` | `true` | If `true`, it enables TypeScript checker. | +| `memoryLimit` | `number` | `2048` | Memory limit for the checker process in MB. If the process exits with the allocation failed error, try to increase this number. | +| `tsconfig` | `string` | `'tsconfig.json'` | Path to the `tsconfig.json` file (path relative to the `compiler.options.context` or absolute path) | +| `context` | `string` | `dirname(configuration.tsconfig)` | The base path for finding files specified in the `tsconfig.json`. Same as the `context` option from the [ts-loader](https://github.com/TypeStrong/ts-loader#context). Useful if you want to keep your `tsconfig.json` in an external package. Keep in mind that **not** having a `tsconfig.json` in your project root can cause different behaviour between `fork-ts-checker-webpack-plugin` and `tsc`. When using editors like `VS Code` it is advised to add a `tsconfig.json` file to the root of the project and extend the config file referenced in option `tsconfig`. | +| `build` | `boolean` | `false` | The equivalent of the `--build` flag for the `tsc` command. To enable `incremental` mode, set it in the `tsconfig.json` file. | +| `mode` | `'readonly'` or `'write-tsbuildinfo'` or `'write-references'` | `'readonly'` | If you use the `babel-loader`, it's recommended to use `write-references` mode to improve initial compilation time. If you use `ts-loader`, it's recommended to use `readonly` mode to not overwrite filed emitted by `ts-loader`. | +| `compilerOptions` | `object` | `{ skipLibCheck: true, skipDefaultLibCheck: true }` | These options will overwrite compiler options from the `tsconfig.json` file. | +| `diagnosticsOptions` | `object` | `{ syntactic: false, semantic: true, declaration: false, global: false }` | Settings to select which diagnostics do we want to perform. | +| `extensions` | `object` | `{}` | See [TypeScript extensions options](#typescript-extensions-options). | #### TypeScript extensions options diff --git a/examples/eslint/package.json b/examples/eslint/package.json index c50552b4..c49e48d8 100644 --- a/examples/eslint/package.json +++ b/examples/eslint/package.json @@ -1,5 +1,5 @@ { - "name": "fork-ts-checker-webpack-plugin-ts-loader-example", + "name": "fork-ts-checker-webpack-plugin-eslint-example", "version": "0.0.0", "main": "dist/index.js", "license": "MIT", diff --git a/package.json b/package.json index ed86a52d..41f3643d 100644 --- a/package.json +++ b/package.json @@ -35,8 +35,7 @@ "lint": "cross-env eslint ./src ./test --ext .ts", "test": "yarn build && yarn test:unit && yarn test:e2e", "test:unit": "cross-env jest unit", - "test:pack": "yarn pack --filename fork-ts-checker-webpack-plugin.tgz", - "test:e2e": "yarn test:pack && cross-env jest e2e --ci --runInBand --bail --verbose", + "test:e2e": "npm pack && cross-env jest e2e --ci --runInBand --bail --verbose", "precommit": "cross-env lint-staged && yarn build && yarn test:unit", "commit": "cross-env git-cz", "semantic-release": "semantic-release" @@ -112,7 +111,6 @@ "tree-kill": "^1.2.2", "ts-jest": "^25.3.1", "typescript": "^3.8.3", - "unixify": "^1.0.0", "webpack": "^4.42.1" }, "engines": { diff --git a/src/ForkTsCheckerWebpackPlugin.ts b/src/ForkTsCheckerWebpackPlugin.ts index 899ce69d..5ed5c7ee 100644 --- a/src/ForkTsCheckerWebpackPlugin.ts +++ b/src/ForkTsCheckerWebpackPlugin.ts @@ -14,6 +14,7 @@ import { createEsLintReporterRpcClient } from './eslint-reporter/reporter/EsLint import { tapStartToConnectAndRunReporter } from './hooks/tapStartToConnectAndRunReporter'; import { tapStopToDisconnectReporter } from './hooks/tapStopToDisconnectReporter'; import { tapDoneToCollectRemoved } from './hooks/tapDoneToCollectRemoved'; +import { tapAfterCompileToAddDependencies } from './hooks/tapAfterCompileToAddDependencies'; import { tapErrorToLogMessage } from './hooks/tapErrorToLogMessage'; import { getForkTsCheckerWebpackPluginHooks } from './hooks/pluginHooks'; @@ -59,6 +60,7 @@ class ForkTsCheckerWebpackPlugin implements webpack.Plugin { tapStartToConnectAndRunReporter(compiler, reporter, configuration, state); tapDoneToCollectRemoved(compiler, configuration, state); + tapAfterCompileToAddDependencies(compiler, configuration); tapStopToDisconnectReporter(compiler, reporter, state); tapErrorToLogMessage(compiler, configuration); } else { diff --git a/src/ForkTsCheckerWebpackPluginOptions.json b/src/ForkTsCheckerWebpackPluginOptions.json index 7b6a3f07..1b5948f5 100644 --- a/src/ForkTsCheckerWebpackPluginOptions.json +++ b/src/ForkTsCheckerWebpackPluginOptions.json @@ -117,6 +117,10 @@ "type": "string", "description": "Path to tsconfig.json. By default plugin uses context or process.cwd() to localize tsconfig.json file." }, + "context": { + "type": "string", + "description": "The base path for finding files specified in the tsconfig.json. Same as context option from the ts-loader." + }, "build": { "type": "boolean", "description": "The equivalent of the `--build` flag from the `tsc`." @@ -126,10 +130,6 @@ "enum": ["readonly", "write-tsbuildinfo", "write-references"], "description": "`readonly` keeps all emitted files in memory, `write-tsbuildinfo` which writes only .tsbuildinfo files and `write-references` which writes both .tsbuildinfo and referenced projects output" }, - "incremental": { - "type": "boolean", - "description": "The equivalent of the `--incremental` flag from the `tsc`." - }, "compilerOptions": { "type": "object", "description": "Custom compilerOptions to be passed to the TypeScript compiler.", diff --git a/src/formatter/WebpackFormatter.ts b/src/formatter/WebpackFormatter.ts index 21244318..0b655890 100644 --- a/src/formatter/WebpackFormatter.ts +++ b/src/formatter/WebpackFormatter.ts @@ -1,19 +1,27 @@ import os from 'os'; import chalk from 'chalk'; -import { relative } from 'path'; +import path from 'path'; import { Formatter } from './Formatter'; import { formatIssueLocation } from '../issue'; -import normalizeSlash from '../utils/path/normalizeSlash'; +import forwardSlash from '../utils/path/forwardSlash'; function createWebpackFormatter(formatter: Formatter, context: string): Formatter { + // mimics webpack error formatter return function webpackFormatter(issue) { + const color = issue.severity === 'warning' ? chalk.yellow.bold : chalk.red.bold; + const severity = issue.severity.toUpperCase(); - const file = issue.file ? normalizeSlash(relative(context, issue.file)) : undefined; - const location = issue.location ? formatIssueLocation(issue.location) : undefined; - const color = issue.severity === 'warning' ? chalk.yellow : chalk.red; - const header = [severity, 'in', file].concat(location ? [location] : []).join(' '); - return [color.bold(header), formatter(issue), ''].join(os.EOL); + if (issue.file) { + let location = forwardSlash(path.relative(context, issue.file)); + if (issue.location) { + location += ` ${formatIssueLocation(issue.location)}`; + } + + return [color(`${severity} in ${location}`), formatter(issue), ''].join(os.EOL); + } else { + return [color(`${severity} in `) + formatter(issue), ''].join(os.EOL); + } }; } diff --git a/src/hooks/tapAfterCompileToAddDependencies.ts b/src/hooks/tapAfterCompileToAddDependencies.ts new file mode 100644 index 00000000..20e8b725 --- /dev/null +++ b/src/hooks/tapAfterCompileToAddDependencies.ts @@ -0,0 +1,17 @@ +import webpack from 'webpack'; +import path from 'path'; +import { ForkTsCheckerWebpackPluginConfiguration } from '../ForkTsCheckerWebpackPluginConfiguration'; + +function tapAfterCompileToAddDependencies( + compiler: webpack.Compiler, + configuration: ForkTsCheckerWebpackPluginConfiguration +) { + compiler.hooks.afterCompile.tap('ForkTsCheckerWebpackPlugin', (compilation) => { + if (configuration.typescript.enabled) { + // watch tsconfig.json file + compilation.fileDependencies.add(path.normalize(configuration.typescript.tsconfig)); + } + }); +} + +export { tapAfterCompileToAddDependencies }; diff --git a/src/issue/IssueMatch.ts b/src/issue/IssueMatch.ts index 0d40e05c..5b28c4de 100644 --- a/src/issue/IssueMatch.ts +++ b/src/issue/IssueMatch.ts @@ -2,7 +2,7 @@ import { Issue } from './index'; import { IssuePredicate } from './IssuePredicate'; import minimatch from 'minimatch'; import path from 'path'; -import normalizeSlash from '../utils/path/normalizeSlash'; +import forwardSlash from '../utils/path/forwardSlash'; type IssueMatch = Partial>; @@ -14,7 +14,7 @@ function createIssuePredicateFromIssueMatch(context: string, match: IssueMatch): const matchesFile = !issue.file || (!!issue.file && - (!match.file || minimatch(normalizeSlash(path.relative(context, issue.file)), match.file))); + (!match.file || minimatch(forwardSlash(path.relative(context, issue.file)), match.file))); return matchesOrigin && matchesSeverity && matchesCode && matchesFile; }; diff --git a/src/issue/IssueWebpackError.ts b/src/issue/IssueWebpackError.ts index 98e61b04..b418e21f 100644 --- a/src/issue/IssueWebpackError.ts +++ b/src/issue/IssueWebpackError.ts @@ -1,7 +1,7 @@ import { relative } from 'path'; import { Issue } from './Issue'; import { formatIssueLocation } from './IssueLocation'; -import normalizeSlash from '../utils/path/normalizeSlash'; +import forwardSlash from '../utils/path/forwardSlash'; class IssueWebpackError extends Error { readonly hideStack = true; @@ -14,11 +14,11 @@ class IssueWebpackError extends Error { // should be a NormalModule instance. // to avoid such a dependency, we do a workaround - error.file will contain formatted location instead if (issue.file) { - const parts = [normalizeSlash(relative(context, issue.file))]; + this.file = forwardSlash(relative(context, issue.file)); + if (issue.location) { - parts.push(formatIssueLocation(issue.location)); + this.file += ` ${formatIssueLocation(issue.location)}`; } - this.file = parts.join(' '); } Error.captureStackTrace(this, this.constructor); diff --git a/src/typescript-reporter/TypeScriptReporterConfiguration.ts b/src/typescript-reporter/TypeScriptReporterConfiguration.ts index a8aef0a2..ba82f798 100644 --- a/src/typescript-reporter/TypeScriptReporterConfiguration.ts +++ b/src/typescript-reporter/TypeScriptReporterConfiguration.ts @@ -1,21 +1,21 @@ import webpack from 'webpack'; import path from 'path'; -import ts from 'typescript'; import { TypeScriptDiagnosticsOptions } from './TypeScriptDiagnosticsOptions'; import { TypeScriptReporterOptions } from './TypeScriptReporterOptions'; import { createTypeScriptVueExtensionConfiguration, TypeScriptVueExtensionConfiguration, } from './extension/vue/TypeScriptVueExtensionConfiguration'; -import normalizeSlash from '../utils/path/normalizeSlash'; interface TypeScriptReporterConfiguration { enabled: boolean; memoryLimit: number; tsconfig: string; build: boolean; + context: string; mode: 'readonly' | 'write-tsbuildinfo' | 'write-references'; - compilerOptions: Partial; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + compilerOptions: any; diagnosticOptions: TypeScriptDiagnosticsOptions; extensions: { vue: TypeScriptVueExtensionConfiguration; @@ -26,41 +26,35 @@ function createTypeScriptReporterConfiguration( compiler: webpack.Compiler, options: TypeScriptReporterOptions | undefined ): TypeScriptReporterConfiguration { - let tsconfig: string = + let tsconfig = typeof options === 'object' ? options.tsconfig || 'tsconfig.json' : 'tsconfig.json'; - // ensure that `tsconfig` is an absolute path with normalized slash - tsconfig = normalizeSlash( + // ensure that `tsconfig` is an absolute normalized path + tsconfig = path.normalize( path.isAbsolute(tsconfig) ? tsconfig : path.resolve(compiler.options.context || process.cwd(), tsconfig) ); - // convert json compilerOptions to ts.CompilerOptions - const convertResults = ts.convertCompilerOptionsFromJson( - { - skipDefaultLibCheck: true, - skipLibCheck: true, - ...(typeof options === 'object' ? options.compilerOptions || {} : {}), - }, - compiler.options.context || process.cwd(), - tsconfig - ); - const convertedOptions = convertResults.options || {}; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { configFilePath, ...compilerOptions } = convertedOptions; + const optionsAsObject: Exclude = + typeof options === 'object' ? options : {}; return { enabled: options !== false, memoryLimit: 2048, build: false, mode: 'readonly', - ...(typeof options === 'object' ? options : {}), + ...optionsAsObject, tsconfig: tsconfig, - compilerOptions: compilerOptions, + context: optionsAsObject.context || path.dirname(tsconfig), + compilerOptions: { + skipDefaultLibCheck: true, + skipLibCheck: true, + ...(optionsAsObject.compilerOptions || {}), + }, extensions: { vue: createTypeScriptVueExtensionConfiguration( - typeof options === 'object' && options.extensions ? options.extensions.vue : undefined + optionsAsObject.extensions ? optionsAsObject.extensions.vue : undefined ), }, diagnosticOptions: { @@ -68,7 +62,7 @@ function createTypeScriptReporterConfiguration( semantic: true, declaration: false, global: false, - ...((typeof options === 'object' && options.diagnosticOptions) || {}), + ...(optionsAsObject.diagnosticOptions || {}), }, }; } diff --git a/src/typescript-reporter/TypeScriptReporterOptions.ts b/src/typescript-reporter/TypeScriptReporterOptions.ts index 54372880..697537ee 100644 --- a/src/typescript-reporter/TypeScriptReporterOptions.ts +++ b/src/typescript-reporter/TypeScriptReporterOptions.ts @@ -7,6 +7,7 @@ type TypeScriptReporterOptions = enabled?: boolean; memoryLimit?: number; tsconfig?: string; + context?: string; build?: boolean; mode?: 'readonly' | 'write-tsbuildinfo' | 'write-references'; compilerOptions?: object; diff --git a/src/typescript-reporter/issue/TypeScriptIssueFactory.ts b/src/typescript-reporter/issue/TypeScriptIssueFactory.ts index 6dfb8506..0b8602c3 100644 --- a/src/typescript-reporter/issue/TypeScriptIssueFactory.ts +++ b/src/typescript-reporter/issue/TypeScriptIssueFactory.ts @@ -43,8 +43,10 @@ function createIssueFromTsDiagnostic(diagnostic: ts.Diagnostic): Issue { }; } -function createIssuesFromTsDiagnostics(diagnostic: ts.Diagnostic[]): Issue[] { - return deduplicateAndSortIssues(diagnostic.map(createIssueFromTsDiagnostic)); +function createIssuesFromTsDiagnostics(diagnostics: ts.Diagnostic[]): Issue[] { + return deduplicateAndSortIssues( + diagnostics.map((diagnostic) => createIssueFromTsDiagnostic(diagnostic)) + ); } export { createIssuesFromTsDiagnostics }; diff --git a/src/typescript-reporter/reporter/ControlledTypeScriptSystem.ts b/src/typescript-reporter/reporter/ControlledTypeScriptSystem.ts index dfaab09f..c0d5fa5f 100644 --- a/src/typescript-reporter/reporter/ControlledTypeScriptSystem.ts +++ b/src/typescript-reporter/reporter/ControlledTypeScriptSystem.ts @@ -1,7 +1,7 @@ import * as ts from 'typescript'; import { dirname } from 'path'; import { createPassiveFileSystem } from '../file-system/PassiveFileSystem'; -import normalizeSlash from '../../utils/path/normalizeSlash'; +import forwardSlash from '../../utils/path/forwardSlash'; import { createRealFileSystem } from '../file-system/RealFileSystem'; interface ControlledTypeScriptSystem extends ts.System { @@ -82,7 +82,7 @@ function createControlledTypeScriptSystem( const fileWatchers = fileWatchersMap.get(normalizedPath); if (fileWatchers) { // typescript expects normalized paths with posix forward slash - fileWatchers.forEach((fileWatcher) => fileWatcher(normalizeSlash(normalizedPath), event)); + fileWatchers.forEach((fileWatcher) => fileWatcher(forwardSlash(normalizedPath), event)); } } @@ -93,7 +93,7 @@ function createControlledTypeScriptSystem( const directoryWatchers = directoryWatchersMap.get(directory); if (directoryWatchers) { directoryWatchers.forEach((directoryWatcher) => - directoryWatcher(normalizeSlash(normalizedPath)) + directoryWatcher(forwardSlash(normalizedPath)) ); } @@ -101,7 +101,7 @@ function createControlledTypeScriptSystem( const recursiveDirectoryWatchers = recursiveDirectoryWatchersMap.get(directory); if (recursiveDirectoryWatchers) { recursiveDirectoryWatchers.forEach((recursiveDirectoryWatcher) => - recursiveDirectoryWatcher(normalizeSlash(normalizedPath)) + recursiveDirectoryWatcher(forwardSlash(normalizedPath)) ); } diff --git a/src/typescript-reporter/reporter/ControlledWatchCompilerHost.ts b/src/typescript-reporter/reporter/ControlledWatchCompilerHost.ts index 0dd8493e..0ef46d61 100644 --- a/src/typescript-reporter/reporter/ControlledWatchCompilerHost.ts +++ b/src/typescript-reporter/reporter/ControlledWatchCompilerHost.ts @@ -3,40 +3,25 @@ import { TypeScriptHostExtension } from '../extension/TypeScriptExtension'; import { ControlledTypeScriptSystem } from './ControlledTypeScriptSystem'; function createControlledWatchCompilerHost( - configFileName: string, - optionsToExtend: ts.CompilerOptions | undefined, + parsedCommandLine: ts.ParsedCommandLine, system: ControlledTypeScriptSystem, createProgram?: ts.CreateProgram, reportDiagnostic?: ts.DiagnosticReporter, reportWatchStatus?: ts.WatchStatusReporter, afterProgramCreate?: (program: TProgram) => void, hostExtensions: TypeScriptHostExtension[] = [] -): ts.WatchCompilerHostOfConfigFile { +): ts.WatchCompilerHostOfFilesAndCompilerOptions { const baseWatchCompilerHost = ts.createWatchCompilerHost( - configFileName, - optionsToExtend, + parsedCommandLine.fileNames, + parsedCommandLine.options, system, createProgram, reportDiagnostic, - reportWatchStatus + reportWatchStatus, + parsedCommandLine.projectReferences ); - const parsedCommendLine = ts.getParsedCommandLineOfConfigFile( - configFileName, - optionsToExtend || {}, - { - fileExists: baseWatchCompilerHost.fileExists, - readFile: baseWatchCompilerHost.readFile, - readDirectory: baseWatchCompilerHost.readDirectory, - useCaseSensitiveFileNames: baseWatchCompilerHost.useCaseSensitiveFileNames(), - getCurrentDirectory: baseWatchCompilerHost.getCurrentDirectory, - trace: baseWatchCompilerHost.trace, - // it's already registered in the watchCompilerHost - onUnRecoverableConfigFileDiagnostic: () => null, - } - ); - - let controlledWatchCompilerHost: ts.WatchCompilerHostOfConfigFile = { + let controlledWatchCompilerHost: ts.WatchCompilerHostOfFilesAndCompilerOptions = { ...baseWatchCompilerHost, createProgram( rootNames: ReadonlyArray | undefined, @@ -48,18 +33,12 @@ function createControlledWatchCompilerHost( ): TProgram { // as compilerHost is optional, ensure that we have it if (!compilerHost) { - if (!options) { - options = parsedCommendLine ? parsedCommendLine.options : undefined; - } - - if (options) { - compilerHost = ts.createCompilerHost(options); - } + compilerHost = ts.createCompilerHost(options || parsedCommandLine.options); } hostExtensions.forEach((hostExtension) => { if (compilerHost && hostExtension.extendCompilerHost) { - compilerHost = hostExtension.extendCompilerHost(compilerHost, parsedCommendLine); + compilerHost = hostExtension.extendCompilerHost(compilerHost, parsedCommandLine); } }); @@ -95,8 +74,8 @@ function createControlledWatchCompilerHost( if (hostExtension.extendWatchCompilerHost) { controlledWatchCompilerHost = hostExtension.extendWatchCompilerHost< TProgram, - ts.WatchCompilerHostOfConfigFile - >(controlledWatchCompilerHost, parsedCommendLine); + ts.WatchCompilerHostOfFilesAndCompilerOptions + >(controlledWatchCompilerHost, parsedCommandLine); } }); diff --git a/src/typescript-reporter/reporter/ControlledWatchSolutionBuilderHost.ts b/src/typescript-reporter/reporter/ControlledWatchSolutionBuilderHost.ts index a1ccddc9..b3f0340c 100644 --- a/src/typescript-reporter/reporter/ControlledWatchSolutionBuilderHost.ts +++ b/src/typescript-reporter/reporter/ControlledWatchSolutionBuilderHost.ts @@ -4,8 +4,7 @@ import { TypeScriptHostExtension } from '../extension/TypeScriptExtension'; import { ControlledTypeScriptSystem } from './ControlledTypeScriptSystem'; function createControlledWatchSolutionBuilderHost( - configFileName: string, - optionsToExtend: ts.CompilerOptions | undefined, + parsedCommandLine: ts.ParsedCommandLine, system: ControlledTypeScriptSystem, createProgram?: ts.CreateProgram, reportDiagnostic?: ts.DiagnosticReporter, @@ -16,8 +15,7 @@ function createControlledWatchSolutionBuilderHost { const controlledWatchCompilerHost = createControlledWatchCompilerHost( - configFileName, - optionsToExtend, + parsedCommandLine, system, createProgram, reportDiagnostic, @@ -60,27 +58,12 @@ function createControlledWatchSolutionBuilderHost null, - } - ); - hostExtensions.forEach((hostExtension) => { if (hostExtension.extendWatchSolutionBuilderHost) { controlledWatchSolutionBuilderHost = hostExtension.extendWatchSolutionBuilderHost< TProgram, ts.SolutionBuilderWithWatchHost - >(controlledWatchSolutionBuilderHost, parsedCommendLine); + >(controlledWatchSolutionBuilderHost, parsedCommandLine); } }); diff --git a/src/typescript-reporter/reporter/TypeScriptConfigurationParser.ts b/src/typescript-reporter/reporter/TypeScriptConfigurationParser.ts new file mode 100644 index 00000000..7aad1ea8 --- /dev/null +++ b/src/typescript-reporter/reporter/TypeScriptConfigurationParser.ts @@ -0,0 +1,29 @@ +import * as ts from 'typescript'; + +function parseTypeScriptConfiguration( + configFileName: string, + configFileContext: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + customCompilerOptions: any, + parseConfigFileHost: ts.ParseConfigFileHost +): ts.ParsedCommandLine { + // convert jsonCompilerOptions to ts.CompilerOptions + const customCompilerOptionsConvertResults = ts.convertCompilerOptionsFromJson( + customCompilerOptions, + configFileContext + ); + + const parsedConfigFile = ts.parseJsonSourceFileConfigFileContent( + ts.readJsonConfigFile(configFileName, parseConfigFileHost.readFile), + parseConfigFileHost, + configFileContext, + customCompilerOptionsConvertResults.options || {} + ); + if (customCompilerOptionsConvertResults.errors) { + parsedConfigFile.errors.push(...customCompilerOptionsConvertResults.errors); + } + + return parsedConfigFile; +} + +export { parseTypeScriptConfiguration }; diff --git a/src/typescript-reporter/reporter/TypeScriptReporter.ts b/src/typescript-reporter/reporter/TypeScriptReporter.ts index 3626d76b..58a76f3e 100644 --- a/src/typescript-reporter/reporter/TypeScriptReporter.ts +++ b/src/typescript-reporter/reporter/TypeScriptReporter.ts @@ -1,4 +1,5 @@ import * as ts from 'typescript'; +import path from 'path'; import { FilesChange, Reporter } from '../../reporter'; import { createIssuesFromTsDiagnostics } from '../issue/TypeScriptIssueFactory'; import { TypeScriptReporterConfiguration } from '../TypeScriptReporterConfiguration'; @@ -10,13 +11,15 @@ import { ControlledTypeScriptSystem, createControlledTypeScriptSystem, } from './ControlledTypeScriptSystem'; +import { parseTypeScriptConfiguration } from './TypeScriptConfigurationParser'; function createTypeScriptReporter(configuration: TypeScriptReporterConfiguration): Reporter { const extensions: TypeScriptExtension[] = []; let system: ControlledTypeScriptSystem | undefined; + let parsedConfiguration: ts.ParsedCommandLine | undefined; let watchCompilerHost: - | ts.WatchCompilerHostOfConfigFile + | ts.WatchCompilerHostOfFilesAndCompilerOptions | undefined; let watchSolutionBuilderHost: | ts.SolutionBuilderWithWatchHost @@ -37,13 +40,6 @@ function createTypeScriptReporter(configuration: TypeScriptReporterConfiguration function getDiagnosticsOfBuilderProgram(builderProgram: ts.BuilderProgram) { const diagnostics: ts.Diagnostic[] = []; - if (typeof builderProgram.getConfigFileParsingDiagnostics === 'function') { - diagnostics.push(...builderProgram.getConfigFileParsingDiagnostics()); - } - if (typeof builderProgram.getOptionsDiagnostics === 'function') { - diagnostics.push(...builderProgram.getOptionsDiagnostics()); - } - if (configuration.diagnosticOptions.syntactic) { diagnostics.push(...builderProgram.getSyntacticDiagnostics()); } @@ -69,13 +65,66 @@ function createTypeScriptReporter(configuration: TypeScriptReporterConfiguration // clear cache to be ready for next iteration and to free memory system.clearCache(); + if ( + [...changedFiles, ...deletedFiles] + .map((affectedFile) => path.normalize(affectedFile)) + .includes(path.normalize(configuration.tsconfig)) + ) { + // we need to re-create programs + parsedConfiguration = undefined; + watchCompilerHost = undefined; + watchSolutionBuilderHost = undefined; + watchProgram = undefined; + solutionBuilder = undefined; + + diagnosticsPerProject.clear(); + } + + if (!parsedConfiguration) { + const parseConfigurationDiagnostics: ts.Diagnostic[] = []; + + parsedConfiguration = parseTypeScriptConfiguration( + configuration.tsconfig, + configuration.context, + configuration.compilerOptions, + { + ...system, + onUnRecoverableConfigFileDiagnostic: (diagnostic) => { + parseConfigurationDiagnostics.push(diagnostic); + }, + } + ); + if (parsedConfiguration.errors) { + parseConfigurationDiagnostics.push(...parsedConfiguration.errors); + } + + // report configuration diagnostics and exit + if (parseConfigurationDiagnostics.length) { + parsedConfiguration = undefined; + let issues = createIssuesFromTsDiagnostics(parseConfigurationDiagnostics); + + issues.forEach((issue) => { + if (!issue.file) { + issue.file = configuration.tsconfig; + } + }); + + extensions.forEach((extension) => { + if (extension.extendIssues) { + issues = extension.extendIssues(issues); + } + }); + + return issues; + } + } + if (configuration.build) { // solution builder case // ensure watch solution builder host exists if (!watchSolutionBuilderHost) { watchSolutionBuilderHost = createControlledWatchSolutionBuilderHost( - configuration.tsconfig, - configuration.compilerOptions as ts.CompilerOptions, // assume that these are valid ts.CompilerOptions + parsedConfiguration, system, ts.createSemanticDiagnosticsBuilderProgram, undefined, @@ -108,8 +157,7 @@ function createTypeScriptReporter(configuration: TypeScriptReporterConfiguration // ensure watch compiler host exists if (!watchCompilerHost) { watchCompilerHost = createControlledWatchCompilerHost( - configuration.tsconfig, - configuration.compilerOptions as ts.CompilerOptions, // assume that these are valid ts.CompilerOptions + parsedConfiguration, system, ts.createSemanticDiagnosticsBuilderProgram, undefined, diff --git a/src/utils/path/forwardSlash.ts b/src/utils/path/forwardSlash.ts new file mode 100644 index 00000000..df1a0a7b --- /dev/null +++ b/src/utils/path/forwardSlash.ts @@ -0,0 +1,11 @@ +import path from 'path'; + +/** + * Replaces backslashes with one forward slash + * @param input + */ +function forwardSlash(input: string): string { + return path.normalize(input).replace(/\\+/g, '/'); +} + +export default forwardSlash; diff --git a/src/utils/path/normalizeSlash.ts b/src/utils/path/normalizeSlash.ts deleted file mode 100644 index ea96c69e..00000000 --- a/src/utils/path/normalizeSlash.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Replaces backslashes with one forward slash - * @param path - */ -function normalizeSlash(path: string): string { - return path.replace(/\\+/g, '/'); -} - -export default normalizeSlash; diff --git a/test/e2e/TypeScriptConfigurationChange.spec.ts b/test/e2e/TypeScriptConfigurationChange.spec.ts new file mode 100644 index 00000000..0c28598a --- /dev/null +++ b/test/e2e/TypeScriptConfigurationChange.spec.ts @@ -0,0 +1,68 @@ +import { createSandbox, FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION, Sandbox } from './sandbox/Sandbox'; +import { readFixture } from './sandbox/Fixture'; +import { join } from 'path'; +import { + createWebpackDevServerDriver, + WEBPACK_CLI_VERSION, + WEBPACK_DEV_SERVER_VERSION, +} from './sandbox/WebpackDevServerDriver'; + +describe('TypeScript Configuration Change', () => { + let sandbox: Sandbox; + + beforeAll(async () => { + sandbox = await createSandbox(); + }); + + beforeEach(async () => { + await sandbox.reset(); + }); + + afterAll(async () => { + await sandbox.cleanup(); + }); + + it.each([ + { async: true, webpack: '~4.0.0', typescript: '2.7.1', tsloader: '^5.0.0' }, + { async: false, webpack: '^4.0.0', typescript: '~3.0.0', tsloader: '^6.0.0' }, + { async: true, webpack: '^5.0.0-beta.16', typescript: '~3.6.0', tsloader: '^7.0.0' }, + { async: false, webpack: '^5.0.0-beta.16', typescript: '~3.8.0', tsloader: '^6.0.0' }, + ])( + 'change in the tsconfig.json affects compilation for %p', + async ({ async, webpack, typescript, tsloader }) => { + await sandbox.load([ + await readFixture(join(__dirname, 'fixtures/environment/typescript-basic.fixture'), { + FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION: JSON.stringify( + FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION + ), + TS_LOADER_VERSION: JSON.stringify(tsloader), + TYPESCRIPT_VERSION: JSON.stringify(typescript), + WEBPACK_VERSION: JSON.stringify(webpack), + WEBPACK_CLI_VERSION: JSON.stringify(WEBPACK_CLI_VERSION), + WEBPACK_DEV_SERVER_VERSION: JSON.stringify(WEBPACK_DEV_SERVER_VERSION), + ASYNC: JSON.stringify(async), + }), + await readFixture(join(__dirname, 'fixtures/implementation/typescript-basic.fixture')), + ]); + + const driver = createWebpackDevServerDriver( + sandbox.spawn('npm run webpack-dev-server'), + async + ); + + // first compilation is successful + await driver.waitForNoErrors(); + + // change available libraries + await sandbox.patch('tsconfig.json', '"lib": ["ES6", "DOM"]', '"lib": ["ES6"],'); + + const errors = await driver.waitForErrors(); + expect(errors.length).toBeGreaterThan(0); + + // revert the change + await sandbox.patch('tsconfig.json', '"lib": ["ES6"]', '"lib": ["DOM", "ES6"],'); + + await driver.waitForNoErrors(); + } + ); +}); diff --git a/test/e2e/TypeScriptContextOption.spec.ts b/test/e2e/TypeScriptContextOption.spec.ts new file mode 100644 index 00000000..da837f6d --- /dev/null +++ b/test/e2e/TypeScriptContextOption.spec.ts @@ -0,0 +1,128 @@ +import { readFixture } from './sandbox/Fixture'; +import { join } from 'path'; +import { createSandbox, FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION, Sandbox } from './sandbox/Sandbox'; +import { + createWebpackDevServerDriver, + WEBPACK_CLI_VERSION, + WEBPACK_DEV_SERVER_VERSION, +} from './sandbox/WebpackDevServerDriver'; + +describe('TypeScript Context Option', () => { + let sandbox: Sandbox; + + beforeAll(async () => { + sandbox = await createSandbox(); + }); + + beforeEach(async () => { + await sandbox.reset(); + }); + + afterAll(async () => { + await sandbox.cleanup(); + }); + + it.each([ + { async: true, typescript: '2.7.1' }, + { async: false, typescript: '~3.0.0' }, + { async: true, typescript: '~3.6.0' }, + { async: false, typescript: '~3.8.0' }, + ])('uses context to resolve project files for %p', async ({ async, typescript }) => { + await sandbox.load([ + await readFixture(join(__dirname, 'fixtures/environment/typescript-basic.fixture'), { + FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION: JSON.stringify( + FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION + ), + TS_LOADER_VERSION: JSON.stringify('^7.0.0'), + TYPESCRIPT_VERSION: JSON.stringify(typescript), + WEBPACK_VERSION: JSON.stringify('^4.0.0'), + WEBPACK_CLI_VERSION: JSON.stringify(WEBPACK_CLI_VERSION), + WEBPACK_DEV_SERVER_VERSION: JSON.stringify(WEBPACK_DEV_SERVER_VERSION), + ASYNC: JSON.stringify(async), + }), + await readFixture(join(__dirname, 'fixtures/implementation/typescript-basic.fixture')), + ]); + + // update sandbox to use context option + await sandbox.remove('tsconfig.json'); + await sandbox.write( + 'build/tsconfig.json', + JSON.stringify({ + compilerOptions: { + target: 'es5', + module: 'commonjs', + lib: ['ES6', 'DOM'], + moduleResolution: 'node', + esModuleInterop: true, + skipLibCheck: true, + skipDefaultLibCheck: true, + strict: true, + baseUrl: './src', + outDir: './dist', + }, + include: ['./src'], + exclude: ['node_modules'], + }) + ); + await sandbox.patch( + 'webpack.config.js', + ' logger: {', + [ + ' typescript: {', + ' enabled: true,', + ' tsconfig: "build/tsconfig.json",', + ' context: __dirname,', + ' },', + ' logger: {', + ].join('\n') + ); + await sandbox.patch( + 'webpack.config.js', + ' transpileOnly: true', + [ + ' transpileOnly: true,', + ' configFile: "build/tsconfig.json",', + ' context: __dirname,', + ].join('\n') + ); + + const driver = createWebpackDevServerDriver(sandbox.spawn('npm run webpack-dev-server'), async); + + // first compilation is successful + await driver.waitForNoErrors(); + + // then we introduce semantic error by removing "firstName" and "lastName" from the User model + await sandbox.patch( + 'src/model/User.ts', + [' firstName?: string;', ' lastName?: string;'].join('\n'), + '' + ); + + // we should receive 2 semantic errors + const errors = await driver.waitForErrors(); + expect(errors).toEqual([ + [ + 'ERROR in src/model/User.ts 11:16-25', + "TS2339: Property 'firstName' does not exist on type 'User'.", + ' 9 | ', + ' 10 | function getUserName(user: User): string {', + ' > 11 | return [user.firstName, user.lastName]', + ' | ^^^^^^^^^', + ' 12 | .filter(name => name !== undefined)', + " 13 | .join(' ');", + ' 14 | }', + ].join('\n'), + [ + 'ERROR in src/model/User.ts 11:32-40', + "TS2339: Property 'lastName' does not exist on type 'User'.", + ' 9 | ', + ' 10 | function getUserName(user: User): string {', + ' > 11 | return [user.firstName, user.lastName]', + ' | ^^^^^^^^', + ' 12 | .filter(name => name !== undefined)', + " 13 | .join(' ');", + ' 14 | }', + ].join('\n'), + ]); + }); +}); diff --git a/test/e2e/sandbox/Sandbox.ts b/test/e2e/sandbox/Sandbox.ts index d982ef78..aa7f417f 100644 --- a/test/e2e/sandbox/Sandbox.ts +++ b/test/e2e/sandbox/Sandbox.ts @@ -276,12 +276,12 @@ async function createSandbox(): Promise { const FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION = join( resolve(__dirname, '../../..'), - 'fork-ts-checker-webpack-plugin.tgz' + 'fork-ts-checker-webpack-plugin-0.0.0-semantic-release.tgz' ); if (!fs.pathExistsSync(FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION)) { throw new Error( - `Cannot find ${FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION} file. To run e2e test, execute "yarn pack --filename fork-ts-checker-webpack-plugin.tgz" command before.` + `Cannot find ${FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION} file. To run e2e test, execute "npm pack" command before.` ); } diff --git a/test/unit/typescript-reporter/TypeScriptReporterConfiguration.spec.ts b/test/unit/typescript-reporter/TypeScriptReporterConfiguration.spec.ts index e15ffa23..b404efd1 100644 --- a/test/unit/typescript-reporter/TypeScriptReporterConfiguration.spec.ts +++ b/test/unit/typescript-reporter/TypeScriptReporterConfiguration.spec.ts @@ -1,16 +1,18 @@ import webpack from 'webpack'; -import unixify from 'unixify'; +import path from 'path'; import { TypeScriptReporterConfiguration } from 'lib/typescript-reporter/TypeScriptReporterConfiguration'; import { TypeScriptReporterOptions } from 'lib/typescript-reporter/TypeScriptReporterOptions'; describe('typescript-reporter/TypeScriptsReporterConfiguration', () => { let compiler: webpack.Compiler; let createTypeScriptVueExtensionConfiguration: jest.Mock; + const context = '/webpack/context'; const configuration: TypeScriptReporterConfiguration = { enabled: true, memoryLimit: 2048, - tsconfig: '/webpack/context/tsconfig.json', + tsconfig: path.normalize(path.resolve(context, 'tsconfig.json')), + context: path.normalize(path.dirname(path.resolve(context, 'tsconfig.json'))), build: false, mode: 'readonly', compilerOptions: { @@ -34,7 +36,7 @@ describe('typescript-reporter/TypeScriptsReporterConfiguration', () => { beforeEach(() => { compiler = { options: { - context: '/webpack/context', + context, }, } as webpack.Compiler; createTypeScriptVueExtensionConfiguration = jest.fn(() => ({ @@ -58,7 +60,10 @@ describe('typescript-reporter/TypeScriptsReporterConfiguration', () => { [{ memoryLimit: 512 }, { ...configuration, memoryLimit: 512 }], [ { tsconfig: 'tsconfig.another.json' }, - { ...configuration, tsconfig: '/webpack/context/tsconfig.another.json' }, + { + ...configuration, + tsconfig: path.normalize(path.resolve(context, 'tsconfig.another.json')), + }, ], [{ build: true }, { ...configuration, build: true }], [{ mode: 'write-tsbuildinfo' }, { ...configuration, mode: 'write-tsbuildinfo' }], @@ -87,9 +92,7 @@ describe('typescript-reporter/TypeScriptsReporterConfiguration', () => { options as TypeScriptReporterOptions ); - expect({ ...configuration, tsconfig: unixify(configuration.tsconfig) }).toEqual( - expectedConfiguration - ); + expect(configuration).toEqual(expectedConfiguration); }); it('passes vue options to the vue extension', async () => { diff --git a/test/unit/typescript-reporter/TypeScriptSupport.spec.ts b/test/unit/typescript-reporter/TypeScriptSupport.spec.ts index 30ee3f9e..465770b8 100644 --- a/test/unit/typescript-reporter/TypeScriptSupport.spec.ts +++ b/test/unit/typescript-reporter/TypeScriptSupport.spec.ts @@ -9,6 +9,7 @@ describe('typescript-reporter/TypeScriptSupport', () => { configuration = { tsconfig: './tsconfig.json', + context: '.', compilerOptions: {}, build: false, mode: 'readonly', diff --git a/yarn.lock b/yarn.lock index 44578ee7..b5fecb7b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9126,13 +9126,6 @@ universalify@^1.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== -unixify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unixify/-/unixify-1.0.0.tgz#3a641c8c2ffbce4da683a5c70f03a462940c2090" - integrity sha1-OmQcjC/7zk2mg6XHDwOkYpQMIJA= - dependencies: - normalize-path "^2.1.1" - unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"