From cf718647545d12c89608bbed9973c2106b306ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Nison?= Date: Thu, 18 Apr 2019 19:03:06 +0100 Subject: [PATCH 1/8] Adds support for custom compilers to CompilerHost --- src/CompilerHost.ts | 86 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/src/CompilerHost.ts b/src/CompilerHost.ts index fba29832..56e15bfd 100644 --- a/src/CompilerHost.ts +++ b/src/CompilerHost.ts @@ -3,6 +3,22 @@ import * as ts from 'typescript'; // Imported for types alone import { LinkedList } from './LinkedList'; import { VueProgram } from './VueProgram'; +export type ResolveModuleName = ( + typescript: typeof ts, + moduleName: string, + containingFile: string, + compilerOptions: ts.CompilerOptions, + moduleResolutionHost: ts.ModuleResolutionHost +) => ts.ResolvedModuleWithFailedLookupLocations; + +export type ResolveTypeReferenceDirective = ( + typescript: typeof ts, + typeDirectiveName: string, + containingFile: string, + compilerOptions: ts.CompilerOptions, + moduleResolutionHost: ts.ModuleResolutionHost +) => ts.ResolvedTypeReferenceDirectiveWithFailedLookupLocations; + interface DirectoryWatchDelaySlot { events: { fileName: string }[]; callback: ts.DirectoryWatcherCallback; @@ -51,11 +67,16 @@ export class CompilerHost private compilationStarted = false; + private readonly resolveModuleName: ResolveModuleName; + private readonly resolveTypeReferenceDirective: ResolveTypeReferenceDirective; + constructor( private typescript: typeof ts, programConfigFile: string, compilerOptions: ts.CompilerOptions, - checkSyntacticErrors: boolean + checkSyntacticErrors: boolean, + resolveModuleName?: ResolveModuleName, + resolveTypeReferenceDirective?: ResolveTypeReferenceDirective ) { this.tsHost = typescript.createWatchCompilerHost( programConfigFile, @@ -75,6 +96,42 @@ export class CompilerHost this.configFileName = this.tsHost.configFileName; this.optionsToExtend = this.tsHost.optionsToExtend || {}; + + // tslint:disable-next-line:no-shadowed-variable + this.resolveModuleName = + resolveModuleName || + (( + typescript, + moduleName, + containingFile, + compilerOptions, + moduleResolutionHost + ) => { + return typescript.resolveModuleName( + moduleName, + containingFile, + compilerOptions, + moduleResolutionHost + ); + }); + + // tslint:disable-next-line:no-shadowed-variable + this.resolveTypeReferenceDirective = + resolveTypeReferenceDirective || + (( + typescript, + typeDirectiveName, + containingFile, + compilerOptions, + moduleResolutionHost + ) => { + return typescript.resolveTypeReferenceDirective( + typeDirectiveName, + containingFile, + compilerOptions, + moduleResolutionHost + ); + }); } public async processChanges(): Promise<{ @@ -346,6 +403,33 @@ export class CompilerHost this.afterCompile(); } + public resolveModuleNames(moduleNames: string[], containingFile: string) { + return moduleNames.map(moduleName => { + return this.resolveModuleName( + this.typescript, + moduleName, + containingFile, + this.optionsToExtend, + this + ).resolvedModule; + }); + } + + public resolveTypeReferenceDirectives( + typeDirectiveNames: string[], + containingFile: string + ) { + return typeDirectiveNames.map(typeDirectiveName => { + return this.resolveTypeReferenceDirective( + this.typescript, + typeDirectiveName, + containingFile, + this.optionsToExtend, + this + ).resolvedTypeReferenceDirective; + }); + } + // the functions below are use internally by typescript. we cannot use non-emitting version of incremental watching API // because it is // - much slower for some reason, From ae4a6d157840eb5335cdd173413f403907f25229 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Nison?= Date: Thu, 18 Apr 2019 21:16:04 +0100 Subject: [PATCH 2/8] Attaches the options to the plugin --- README.md | 62 ++++++++++++++++++++---------------- src/ApiIncrementalChecker.ts | 14 ++++++-- src/CompilerHost.ts | 6 ++-- src/index.ts | 10 ++++++ src/service.ts | 9 +++++- 5 files changed, 67 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index dd57ec4c..78f4264d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build Status](https://travis-ci.org/Realytics/fork-ts-checker-webpack-plugin.svg?branch=master)](https://travis-ci.org/Realytics/fork-ts-checker-webpack-plugin) Webpack plugin that runs typescript type checker on a separate process. - + ## Installation This plugin requires minimum **webpack 2.3**, **typescript 2.1** and optionally **tslint 4.0** ```sh @@ -23,7 +23,7 @@ var webpackConfig = { loader: 'ts-loader', options: { // disable type checker - we will use it in fork plugin - transpileOnly: true + transpileOnly: true } } ] @@ -39,21 +39,21 @@ There is already similar solution - [awesome-typescript-loader](https://github.c 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 use [tslint](https://palantir.github.io/tslint) and we wanted to run this, along with type checker, in a separate process. -This is why we've created this plugin. To provide better performance, plugin reuses Abstract Syntax Trees between compilations and shares +This is why we've created this plugin. To provide better performance, plugin reuses Abstract Syntax Trees between compilations and shares these trees with tslint. It can be scaled with a multi-process mode to utilize maximum CPU power. ## 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 +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. To debug typescript's modules resolution, you can use `tsc --traceResolution` command. ## TSLint -If you have installed [tslint](https://palantir.github.io/tslint), you can enable it by setting `tslint: true` or -`tslint: './path/to/tslint.json'`. We recommend changing `defaultSeverity` to a `"warning"` in `tslint.json` file. +If you have installed [tslint](https://palantir.github.io/tslint), you can enable it by setting `tslint: true` or +`tslint: './path/to/tslint.json'`. We recommend changing `defaultSeverity` to a `"warning"` in `tslint.json` file. It helps to distinguish lints from typescript's diagnostics. ## Options @@ -63,7 +63,7 @@ Path to *tsconfig.json* file. Default: `path.resolve(compiler.options.context, ' * **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: `{}`. -* **tslint** `string | true | undefined`: +* **tslint** `string | true | undefined`: - If `string`, path to *tslint.json* file to check source files against. - If `true`, path to `tslint.json` file will be computed with respect to currently checked file, just like TSLint CLI would do. Suppose you have a project: @@ -81,13 +81,13 @@ Allows overriding TypeScript options. Should be specified in the same format as ``` In such a case `src/file.ts` and `src/anotherFile.ts` would be checked against root `tslint.json`, and `src/lib/someHelperFile.ts` would be checked against `src/lib/tslint.json`. - + Default: `undefined`. * **tslintAutoFix** `boolean `: Passes on `--fix` flag while running `tslint` to auto fix linting errors. Default: false. -* **watch** `string | string[]`: +* **watch** `string | string[]`: Directories or files to watch by service. Not necessary but improves performance (reduces number of `fs.stat` calls). * **async** `boolean`: @@ -97,13 +97,13 @@ We recommend to set this to `false` in projects where type checking is faster th * **ignoreDiagnostics** `number[]`: List of typescript diagnostic codes to ignore. -* **ignoreLints** `string[]`: +* **ignoreLints** `string[]`: List of tslint rule names to ignore. * **ignoreLintWarnings** `boolean`: If true, will ignore all lint warnings. -* **reportFiles** `string[]`: +* **reportFiles** `string[]`: Only report errors on files matching these glob patterns. This can be useful when certain types definitions have errors that are not fatal to your application. Default: `[]`. Please note that this may behave unexpectedly if using the incremental API as the incremental API doesn't look for global and semantic errors [if it has already found syntactic errors](https://github.com/Microsoft/TypeScript/blob/89386ddda7dafc63cb35560e05412487f47cc267/src/compiler/watch.ts#L141). ```js @@ -127,25 +127,25 @@ Options passed to formatters (currently only `codeframe` - see [available option * **silent** `boolean`: If `true`, logger will not be used. Default: `false`. -* **checkSyntacticErrors** `boolean`: +* **checkSyntacticErrors** `boolean`: This option is useful if you're using ts-loader in `happyPackMode` with [HappyPack](https://github.com/amireh/happypack) or [thread-loader](https://github.com/webpack-contrib/thread-loader) to parallelise your builds. If `true` it will ensure that the plugin checks for *both* syntactic errors (eg `const array = [{} {}];`) and semantic errors (eg `const x: number = '1';`). By default the plugin only checks for semantic errors. This is because when ts-loader is used in `transpileOnly` mode, ts-loader will still report syntactic errors. When used in `happyPackMode` it does not. Default: `false`. -* **memoryLimit** `number`: +* **memoryLimit** `number`: Memory limit for service process in MB. If service exits with allocation failed error, increase this number. Default: `2048`. * **workers** `number`: -You can split type checking to a few workers to speed-up increment build. **Be careful** - if you don't want to increase build time, you +You can split type checking to a few workers to speed-up increment build. **Be careful** - if you don't want to increase build time, you should keep free 1 core for *build* and 1 core for a *system* *(for example system with 4 CPUs should use max 2 workers)*. Second thing - node doesn't share memory between workers - keep in mind that memory usage will increase. Be aware that in some scenarios increasing workers number **can increase checking time**. Default: `ForkTsCheckerWebpackPlugin.ONE_CPU`. * **vue** `boolean`: -If `true`, the linter and compiler will process VueJs single-file-component (.vue) files. See the +If `true`, the linter and compiler will process VueJs single-file-component (.vue) files. See the [Vue section](https://github.com/Realytics/fork-ts-checker-webpack-plugin#vue) further down for information on how to correctly setup your project. * **useTypescriptIncrementalApi** `boolean`: -If true, the plugin will use incremental compilation API introduced in typescript 2.7. In this mode you can only have 1 -worker, but if the changes in your code are small (like you normally have when you work in 'watch' mode), the compilation +If true, the plugin will use incremental compilation API introduced in typescript 2.7. In this mode you can only have 1 +worker, but if the changes in your code are small (like you normally have when you work in 'watch' mode), the compilation may be much faster, even compared to multi-threaded compilation. Defaults to `true` when working with typescript 3+ and `false` when below 3. The default can be overridden by directly specifying a value. * **measureCompilationTime** `boolean`: @@ -155,7 +155,13 @@ especially if there are other loaders/plugins involved in the compilation. **req * **typescript** `string`: If supplied this is a custom path where `typescript` can be found. Defaults to `require.resolve('typescript')`. -### Pre-computed consts: +* **resolveModuleNameModule** `string`: +If supplied this is a path of a file where the worker can find a working implementation of `resolveModuleName` to use with TypeScript (exported through the `resolveModuleName` symbol). + +* **resolveTypeReferenceDirectiveModule** `string`: +If supplied this is a path of a file where the worker can find a working implementation of `resolveTypeReferenceDirective` to use with TypeScript (exported through the `resolveTypeReferenceDirective` symbol). + +### Pre-computed consts: * `ForkTsCheckerWebpackPlugin.ONE_CPU` - always use one CPU * `ForkTsCheckerWebpackPlugin.ALL_CPUS` - always use all CPUs (will increase build time) * `ForkTsCheckerWebpackPlugin.ONE_CPU_FREE` - leave only one CPU for build (probably will increase build time) @@ -188,7 +194,7 @@ This plugin provides some custom webpack hooks (all are sync): |`fork-ts-checker-service-start`| Service will be started | `tsconfigPath`, `tslintPath`, `watchPaths`, `workersNumber`, `memoryLimit` | |`fork-ts-checker-service-start-error` | Cannot start service | `error` | |`fork-ts-checker-service-out-of-memory`| Service is out of memory | - | -|`fork-ts-checker-receive`| Plugin receives diagnostics and lints from service | `diagnostics`, `lints` | +|`fork-ts-checker-receive`| Plugin receives diagnostics and lints from service | `diagnostics`, `lints` | |`fork-ts-checker-emit`| Service will add errors and warnings to webpack compilation ('build' mode) | `diagnostics`, `lints`, `elapsed` | |`fork-ts-checker-done`| Service finished type checking and webpack finished compilation ('watch' mode) | `diagnostics`, `lints`, `elapsed` | @@ -203,7 +209,7 @@ new ForkTsCheckerWebpackPlugin({ ``` 2. To activate TypeScript in your `.vue` files, you need to ensure your script tag's language attribute is set -to `ts` or `tsx` (also make sure you include the `.vue` extension in all your import statements as shown below): +to `ts` or `tsx` (also make sure you include the `.vue` extension in all your import statements as shown below): ```html ``` -3. Ideally you are also using `ts-loader` (in transpileOnly mode). Your Webpack config rules may look something like this: +3. Ideally you are also using `ts-loader` (in transpileOnly mode). Your Webpack config rules may look something like this: ```js { @@ -231,7 +237,7 @@ import Hello from '@/components/hello.vue' options: vueLoaderConfig }, ``` -4. Add rules to your `tslint.json` and they will be applied to Vue files. For example, you could apply the Standard JS rules [tslint-config-standard](https://github.com/blakeembrey/tslint-config-standard) like this: +4. Add rules to your `tslint.json` and they will be applied to Vue files. For example, you could apply the Standard JS rules [tslint-config-standard](https://github.com/blakeembrey/tslint-config-standard) like this: ```json { @@ -241,7 +247,7 @@ import Hello from '@/components/hello.vue' ] } ``` -5. Ensure your `tsconfig.json` includes .vue files: +5. Ensure your `tsconfig.json` includes .vue files: ```js // tsconfig.json @@ -256,12 +262,12 @@ import Hello from '@/components/hello.vue' } ``` -6. It accepts any wildcard in your TypeScript configuration: +6. It accepts any wildcard in your TypeScript configuration: ```js // tsconfig.json { "compilerOptions": { - + // ... "baseUrl": ".", diff --git a/src/ApiIncrementalChecker.ts b/src/ApiIncrementalChecker.ts index 2945accd..472ecbd3 100644 --- a/src/ApiIncrementalChecker.ts +++ b/src/ApiIncrementalChecker.ts @@ -12,7 +12,11 @@ import { makeGetLinterConfig } from './linterConfigHelpers'; import { NormalizedMessage } from './NormalizedMessage'; -import { CompilerHost } from './CompilerHost'; +import { + CompilerHost, + ResolveModuleName, + ResolveTypeReferenceDirective +} from './CompilerHost'; import { FsHelper } from './FsHelper'; export class ApiIncrementalChecker implements IncrementalCheckerInterface { @@ -41,7 +45,9 @@ export class ApiIncrementalChecker implements IncrementalCheckerInterface { private context: string, private linterConfigFile: string | boolean, private linterAutoFix: boolean, - checkSyntacticErrors: boolean + checkSyntacticErrors: boolean, + resolveModuleName: ResolveModuleName | undefined, + resolveTypeReferenceDirective: ResolveTypeReferenceDirective | undefined ) { this.hasFixedConfig = typeof this.linterConfigFile === 'string'; @@ -51,7 +57,9 @@ export class ApiIncrementalChecker implements IncrementalCheckerInterface { typescript, programConfigFile, compilerOptions, - checkSyntacticErrors + checkSyntacticErrors, + resolveModuleName, + resolveTypeReferenceDirective ); } diff --git a/src/CompilerHost.ts b/src/CompilerHost.ts index 56e15bfd..e11c8106 100644 --- a/src/CompilerHost.ts +++ b/src/CompilerHost.ts @@ -97,13 +97,14 @@ export class CompilerHost this.configFileName = this.tsHost.configFileName; this.optionsToExtend = this.tsHost.optionsToExtend || {}; - // tslint:disable-next-line:no-shadowed-variable this.resolveModuleName = resolveModuleName || (( + // tslint:disable-next-line:no-shadowed-variable typescript, moduleName, containingFile, + // tslint:disable-next-line:no-shadowed-variable compilerOptions, moduleResolutionHost ) => { @@ -115,13 +116,14 @@ export class CompilerHost ); }); - // tslint:disable-next-line:no-shadowed-variable this.resolveTypeReferenceDirective = resolveTypeReferenceDirective || (( + // tslint:disable-next-line:no-shadowed-variable typescript, typeDirectiveName, containingFile, + // tslint:disable-next-line:no-shadowed-variable compilerOptions, moduleResolutionHost ) => { diff --git a/src/index.ts b/src/index.ts index 3d0774ea..f8c4259a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,6 +53,8 @@ interface Options { vue: boolean; useTypescriptIncrementalApi: boolean; measureCompilationTime: boolean; + resolveModuleNameModule: string; + resolveTypeReferenceDirectiveModule: string; } /** @@ -99,6 +101,8 @@ class ForkTsCheckerWebpackPlugin { private colors: Chalk; private formatter: Formatter; private useTypescriptIncrementalApi: boolean; + private resolveModuleNameModule: string | undefined; + private resolveTypeReferenceDirectiveModule: string | undefined; private tsconfigPath?: string; private tslintPath?: string; @@ -155,6 +159,9 @@ class ForkTsCheckerWebpackPlugin { this.silent = options.silent === true; // default false this.async = options.async !== false; // default true this.checkSyntacticErrors = options.checkSyntacticErrors === true; // default false + this.resolveModuleNameModule = options.resolveModuleNameModule; + this.resolveTypeReferenceDirectiveModule = + options.resolveTypeReferenceDirectiveModule; this.workersNumber = options.workers || ForkTsCheckerWebpackPlugin.ONE_CPU; this.memoryLimit = options.memoryLimit || ForkTsCheckerWebpackPlugin.DEFAULT_MEMORY_LIMIT; @@ -590,6 +597,9 @@ class ForkTsCheckerWebpackPlugin { MEMORY_LIMIT: this.memoryLimit, CHECK_SYNTACTIC_ERRORS: this.checkSyntacticErrors, USE_INCREMENTAL_API: this.useTypescriptIncrementalApi === true, + RESOLVE_MODULE_NAME: this.resolveModuleNameModule, + RESOLVE_TYPE_REFERENCE_DIRECTIVE: this + .resolveTypeReferenceDirectiveModule, VUE: this.vue }, stdio: ['inherit', 'inherit', 'inherit', 'ipc'] diff --git a/src/service.ts b/src/service.ts index 90c47306..307aab25 100644 --- a/src/service.ts +++ b/src/service.ts @@ -42,7 +42,14 @@ const checker: IncrementalCheckerInterface = process.env.CONTEXT!, process.env.TSLINT === 'true' ? true : process.env.TSLINT! || false, process.env.TSLINTAUTOFIX === 'true', - process.env.CHECK_SYNTACTIC_ERRORS === 'true' + process.env.CHECK_SYNTACTIC_ERRORS === 'true', + process.env.RESOLVE_MODULE_NAME + ? require(process.env.RESOLVE_MODULE_NAME!).resolveModuleName + : undefined, + process.env.RESOLVE_TYPE_REFERENCE_DIRECTIVE + ? require(process.env.RESOLVE_TYPE_REFERENCE_DIRECTIVE!) + .resolveTypeReferenceDirective + : undefined ) : new IncrementalChecker( typescript, From 58e86bf9eb06adc655824d1db6c5e629870c6c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Nison?= Date: Fri, 19 Apr 2019 11:59:38 +0100 Subject: [PATCH 3/8] Adds support for IncrementalChecker --- src/ApiIncrementalChecker.ts | 7 ++-- src/CompilerHost.ts | 70 +++++++++--------------------------- src/IncrementalChecker.ts | 58 +++++++++++++++++++++++++++--- src/VueProgram.ts | 44 ++++++++++++++++++++++- src/resolution.ts | 63 ++++++++++++++++++++++++++++++++ src/service.ts | 22 +++++++----- 6 files changed, 192 insertions(+), 72 deletions(-) create mode 100644 src/resolution.ts diff --git a/src/ApiIncrementalChecker.ts b/src/ApiIncrementalChecker.ts index 472ecbd3..dfceab58 100644 --- a/src/ApiIncrementalChecker.ts +++ b/src/ApiIncrementalChecker.ts @@ -12,11 +12,8 @@ import { makeGetLinterConfig } from './linterConfigHelpers'; import { NormalizedMessage } from './NormalizedMessage'; -import { - CompilerHost, - ResolveModuleName, - ResolveTypeReferenceDirective -} from './CompilerHost'; +import { CompilerHost } from './CompilerHost'; +import { ResolveModuleName, ResolveTypeReferenceDirective } from './resolution'; import { FsHelper } from './FsHelper'; export class ApiIncrementalChecker implements IncrementalCheckerInterface { diff --git a/src/CompilerHost.ts b/src/CompilerHost.ts index e11c8106..47288bd5 100644 --- a/src/CompilerHost.ts +++ b/src/CompilerHost.ts @@ -2,22 +2,11 @@ import * as ts from 'typescript'; // Imported for types alone import { LinkedList } from './LinkedList'; import { VueProgram } from './VueProgram'; - -export type ResolveModuleName = ( - typescript: typeof ts, - moduleName: string, - containingFile: string, - compilerOptions: ts.CompilerOptions, - moduleResolutionHost: ts.ModuleResolutionHost -) => ts.ResolvedModuleWithFailedLookupLocations; - -export type ResolveTypeReferenceDirective = ( - typescript: typeof ts, - typeDirectiveName: string, - containingFile: string, - compilerOptions: ts.CompilerOptions, - moduleResolutionHost: ts.ModuleResolutionHost -) => ts.ResolvedTypeReferenceDirectiveWithFailedLookupLocations; +import { + ResolveModuleName, + ResolveTypeReferenceDirective, + makeResolutionFunctions +} from './resolution'; interface DirectoryWatchDelaySlot { events: { fileName: string }[]; @@ -75,8 +64,8 @@ export class CompilerHost programConfigFile: string, compilerOptions: ts.CompilerOptions, checkSyntacticErrors: boolean, - resolveModuleName?: ResolveModuleName, - resolveTypeReferenceDirective?: ResolveTypeReferenceDirective + userResolveModuleName?: ResolveModuleName, + userResolveTypeReferenceDirective?: ResolveTypeReferenceDirective ) { this.tsHost = typescript.createWatchCompilerHost( programConfigFile, @@ -97,43 +86,16 @@ export class CompilerHost this.configFileName = this.tsHost.configFileName; this.optionsToExtend = this.tsHost.optionsToExtend || {}; - this.resolveModuleName = - resolveModuleName || - (( - // tslint:disable-next-line:no-shadowed-variable - typescript, - moduleName, - containingFile, - // tslint:disable-next-line:no-shadowed-variable - compilerOptions, - moduleResolutionHost - ) => { - return typescript.resolveModuleName( - moduleName, - containingFile, - compilerOptions, - moduleResolutionHost - ); - }); + const { + resolveModuleName, + resolveTypeReferenceDirective + } = makeResolutionFunctions( + userResolveModuleName, + userResolveTypeReferenceDirective + ); - this.resolveTypeReferenceDirective = - resolveTypeReferenceDirective || - (( - // tslint:disable-next-line:no-shadowed-variable - typescript, - typeDirectiveName, - containingFile, - // tslint:disable-next-line:no-shadowed-variable - compilerOptions, - moduleResolutionHost - ) => { - return typescript.resolveTypeReferenceDirective( - typeDirectiveName, - containingFile, - compilerOptions, - moduleResolutionHost - ); - }); + this.resolveModuleName = resolveModuleName; + this.resolveTypeReferenceDirective = resolveTypeReferenceDirective; } public async processChanges(): Promise<{ diff --git a/src/IncrementalChecker.ts b/src/IncrementalChecker.ts index d284e003..a3492b7f 100644 --- a/src/IncrementalChecker.ts +++ b/src/IncrementalChecker.ts @@ -14,6 +14,11 @@ import { import { WorkSet } from './WorkSet'; import { NormalizedMessage } from './NormalizedMessage'; import { CancellationToken } from './CancellationToken'; +import { + ResolveModuleName, + ResolveTypeReferenceDirective, + makeResolutionFunctions +} from './resolution'; import * as minimatch from 'minimatch'; import { VueProgram } from './VueProgram'; import { FsHelper } from './FsHelper'; @@ -59,7 +64,11 @@ export class IncrementalChecker implements IncrementalCheckerInterface { private workNumber: number = 0, private workDivision: number = 1, private checkSyntacticErrors: boolean = false, - private vue: boolean = false + private vue: boolean = false, + private resolveModuleName: ResolveModuleName | undefined, + private resolveTypeReferenceDirective: + | ResolveTypeReferenceDirective + | undefined ) { this.hasFixedConfig = typeof this.linterConfigFile === 'string'; } @@ -102,11 +111,48 @@ export class IncrementalChecker implements IncrementalCheckerInterface { programConfig: ts.ParsedCommandLine, files: FilesRegister, watcher: FilesWatcher, - oldProgram: ts.Program + oldProgram: ts.Program, + userResolveModuleName: ResolveModuleName | undefined, + userResolveTypeReferenceDirective: ResolveTypeReferenceDirective | undefined ) { const host = typescript.createCompilerHost(programConfig.options); const realGetSourceFile = host.getSourceFile; + const { + resolveModuleName, + resolveTypeReferenceDirective + } = makeResolutionFunctions( + userResolveModuleName, + userResolveTypeReferenceDirective + ); + + host.resolveModuleNames = (moduleNames, containingFile) => { + return moduleNames.map(moduleName => { + return resolveModuleName( + typescript, + moduleName, + containingFile, + programConfig.options, + host + ).resolvedModule; + }); + }; + + host.resolveTypeReferenceDirectives = ( + typeDirectiveNames, + containingFile + ) => { + return typeDirectiveNames.map(typeDirectiveName => { + return resolveTypeReferenceDirective( + typescript, + typeDirectiveName, + containingFile, + programConfig.options, + host + ).resolvedTypeReferenceDirective; + }); + }; + host.getSourceFile = (filePath, languageVersion, onError) => { // first check if watcher is watching file - if not - check it's mtime if (!watcher.isWatchingFile(filePath)) { @@ -215,7 +261,9 @@ export class IncrementalChecker implements IncrementalCheckerInterface { path.dirname(this.programConfigFile), this.files, this.watcher!, - this.program! + this.program!, + this.resolveModuleName, + this.resolveTypeReferenceDirective ); } @@ -233,7 +281,9 @@ export class IncrementalChecker implements IncrementalCheckerInterface { this.programConfig, this.files, this.watcher!, - this.program! + this.program!, + this.resolveModuleName, + this.resolveTypeReferenceDirective ); } diff --git a/src/VueProgram.ts b/src/VueProgram.ts index f5bd7b8b..9ba596c2 100644 --- a/src/VueProgram.ts +++ b/src/VueProgram.ts @@ -4,6 +4,11 @@ import * as path from 'path'; import * as ts from 'typescript'; // import for types alone import { FilesRegister } from './FilesRegister'; import { FilesWatcher } from './FilesWatcher'; +import { + ResolveModuleName, + ResolveTypeReferenceDirective, + makeResolutionFunctions +} from './resolution'; // tslint:disable-next-line:no-implicit-dependencies import * as vueCompiler from 'vue-template-compiler'; @@ -120,11 +125,48 @@ export class VueProgram { basedir: string, files: FilesRegister, watcher: FilesWatcher, - oldProgram: ts.Program + oldProgram: ts.Program, + userResolveModuleName: ResolveModuleName | undefined, + userResolveTypeReferenceDirective: ResolveTypeReferenceDirective | undefined ) { const host = typescript.createCompilerHost(programConfig.options); const realGetSourceFile = host.getSourceFile; + const { + resolveModuleName, + resolveTypeReferenceDirective + } = makeResolutionFunctions( + userResolveModuleName, + userResolveTypeReferenceDirective + ); + + host.resolveModuleNames = (moduleNames, containingFile) => { + return moduleNames.map(moduleName => { + return resolveModuleName( + typescript, + moduleName, + containingFile, + programConfig.options, + host + ).resolvedModule; + }); + }; + + host.resolveTypeReferenceDirectives = ( + typeDirectiveNames, + containingFile + ) => { + return typeDirectiveNames.map(typeDirectiveName => { + return resolveTypeReferenceDirective( + typescript, + typeDirectiveName, + containingFile, + programConfig.options, + host + ).resolvedTypeReferenceDirective; + }); + }; + // We need a host that can parse Vue SFCs (single file components). host.getSourceFile = (filePath, languageVersion, onError) => { // first check if watcher is watching file - if not - check it's mtime diff --git a/src/resolution.ts b/src/resolution.ts new file mode 100644 index 00000000..ef67408a --- /dev/null +++ b/src/resolution.ts @@ -0,0 +1,63 @@ +// tslint:disable-next-line:no-implicit-dependencies +import * as ts from 'typescript'; // Imported for types alone + +export type ResolveModuleName = ( + typescript: typeof ts, + moduleName: string, + containingFile: string, + compilerOptions: ts.CompilerOptions, + moduleResolutionHost: ts.ModuleResolutionHost +) => ts.ResolvedModuleWithFailedLookupLocations; + +export type ResolveTypeReferenceDirective = ( + typescript: typeof ts, + typeDirectiveName: string, + containingFile: string, + compilerOptions: ts.CompilerOptions, + moduleResolutionHost: ts.ModuleResolutionHost +) => ts.ResolvedTypeReferenceDirectiveWithFailedLookupLocations; + +export function makeResolutionFunctions( + resolveModuleName: ResolveModuleName | undefined, + resolveTypeReferenceDirective: ResolveTypeReferenceDirective | undefined +) { + resolveModuleName = + resolveModuleName || + (( + // tslint:disable-next-line:no-shadowed-variable + typescript, + moduleName, + containingFile, + // tslint:disable-next-line:no-shadowed-variable + compilerOptions, + moduleResolutionHost + ) => { + return typescript.resolveModuleName( + moduleName, + containingFile, + compilerOptions, + moduleResolutionHost + ); + }); + + resolveTypeReferenceDirective = + resolveTypeReferenceDirective || + (( + // tslint:disable-next-line:no-shadowed-variable + typescript, + typeDirectiveName, + containingFile, + // tslint:disable-next-line:no-shadowed-variable + compilerOptions, + moduleResolutionHost + ) => { + return typescript.resolveTypeReferenceDirective( + typeDirectiveName, + containingFile, + compilerOptions, + moduleResolutionHost + ); + }); + + return { resolveModuleName, resolveTypeReferenceDirective }; +} diff --git a/src/service.ts b/src/service.ts index 307aab25..dcafc077 100644 --- a/src/service.ts +++ b/src/service.ts @@ -31,6 +31,15 @@ export const createNormalizedMessageFromDiagnostic = makeCreateNormalizedMessage ); export const createNormalizedMessageFromRuleFailure = makeCreateNormalizedMessageFromRuleFailure(); +const resolveModuleName = process.env.RESOLVE_MODULE_NAME + ? require(process.env.RESOLVE_MODULE_NAME!).resolveModuleName + : undefined; +const resolveTypeReferenceDirective = process.env + .RESOLVE_TYPE_REFERENCE_DIRECTIVE + ? require(process.env.RESOLVE_TYPE_REFERENCE_DIRECTIVE!) + .resolveTypeReferenceDirective + : undefined; + const checker: IncrementalCheckerInterface = process.env.USE_INCREMENTAL_API === 'true' ? new ApiIncrementalChecker( @@ -43,13 +52,8 @@ const checker: IncrementalCheckerInterface = process.env.TSLINT === 'true' ? true : process.env.TSLINT! || false, process.env.TSLINTAUTOFIX === 'true', process.env.CHECK_SYNTACTIC_ERRORS === 'true', - process.env.RESOLVE_MODULE_NAME - ? require(process.env.RESOLVE_MODULE_NAME!).resolveModuleName - : undefined, - process.env.RESOLVE_TYPE_REFERENCE_DIRECTIVE - ? require(process.env.RESOLVE_TYPE_REFERENCE_DIRECTIVE!) - .resolveTypeReferenceDirective - : undefined + resolveModuleName, + resolveTypeReferenceDirective ) : new IncrementalChecker( typescript, @@ -64,7 +68,9 @@ const checker: IncrementalCheckerInterface = parseInt(process.env.WORK_NUMBER!, 10) || 0, parseInt(process.env.WORK_DIVISION!, 10) || 1, process.env.CHECK_SYNTACTIC_ERRORS === 'true', - process.env.VUE === 'true' + process.env.VUE === 'true', + resolveModuleName, + resolveTypeReferenceDirective ); async function run(cancellationToken: CancellationToken) { From 311534fe38cb2570ccc6cfab4af638f1fc053508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Nison?= Date: Fri, 19 Apr 2019 12:21:47 +0100 Subject: [PATCH 4/8] Adds a test --- test/integration/index.spec.js | 13 +++++++++++++ test/integration/project/src/defs.d.ts | 1 + test/integration/project/src/export.ts | 1 + test/integration/project/src/weirdResolutions.ts | 5 +++++ .../project/tsconfig-weird-resolutions.json | 8 ++++++++ test/integration/project/tsconfig.json | 5 ++++- test/integration/project/weirdResolver.js | 11 +++++++++++ 7 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 test/integration/project/src/defs.d.ts create mode 100644 test/integration/project/src/export.ts create mode 100644 test/integration/project/src/weirdResolutions.ts create mode 100644 test/integration/project/tsconfig-weird-resolutions.json create mode 100644 test/integration/project/weirdResolver.js diff --git a/test/integration/index.spec.js b/test/integration/index.spec.js index 0648d328..e2206203 100644 --- a/test/integration/index.spec.js +++ b/test/integration/index.spec.js @@ -119,6 +119,19 @@ function makeCommonTests(useTypescriptIncrementalApi) { }); }); + it('should support custom resolution', function(callback) { + var compiler = createCompiler({ + tsconfig: 'tsconfig-weird-resolutions.json', + resolveModuleNameModule: `${__dirname}/project/weirdResolver.js`, + resolveTypeReferenceDirectiveModule: `${__dirname}/project/weirdResolver.js` + }); + + compiler.run(function(err, stats) { + expect(stats.compilation.errors.length).to.be.eq(0); + callback(); + }); + }); + it('should fix linting errors with tslintAutofix flag set to true', function(callback) { const fileName = 'lintingError1'; helpers.testLintAutoFixTest( diff --git a/test/integration/project/src/defs.d.ts b/test/integration/project/src/defs.d.ts new file mode 100644 index 00000000..5bc7fee2 --- /dev/null +++ b/test/integration/project/src/defs.d.ts @@ -0,0 +1 @@ +declare function helloWorld(): void; diff --git a/test/integration/project/src/export.ts b/test/integration/project/src/export.ts new file mode 100644 index 00000000..9d0bfc44 --- /dev/null +++ b/test/integration/project/src/export.ts @@ -0,0 +1 @@ +export const fortyTwo = 42; diff --git a/test/integration/project/src/weirdResolutions.ts b/test/integration/project/src/weirdResolutions.ts new file mode 100644 index 00000000..ed316416 --- /dev/null +++ b/test/integration/project/src/weirdResolutions.ts @@ -0,0 +1,5 @@ +/// + +import { fortyTwo } from 'please-find-my-export'; + +helloWorld(); diff --git a/test/integration/project/tsconfig-weird-resolutions.json b/test/integration/project/tsconfig-weird-resolutions.json new file mode 100644 index 00000000..057019f2 --- /dev/null +++ b/test/integration/project/tsconfig-weird-resolutions.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "lib": ["es2016"], + }, + "include": [ + "src/weirdResolutions.ts" + ] +} diff --git a/test/integration/project/tsconfig.json b/test/integration/project/tsconfig.json index 2a67d98b..e37f5727 100644 --- a/test/integration/project/tsconfig.json +++ b/test/integration/project/tsconfig.json @@ -1,5 +1,8 @@ { "compilerOptions": { "lib": ["es2016"], - } + }, + "exclude": [ + "src/weirdResolutions.ts" + ] } diff --git a/test/integration/project/weirdResolver.js b/test/integration/project/weirdResolver.js new file mode 100644 index 00000000..5cb1e55d --- /dev/null +++ b/test/integration/project/weirdResolver.js @@ -0,0 +1,11 @@ +exports.resolveModuleName = function() { + return { resolvedModule: { resolvedFileName: `${__dirname}/src/export.ts` } }; +}; + +exports.resolveTypeReferenceDirective = function() { + return { + resolvedTypeReferenceDirective: { + resolvedFileName: `${__dirname}/src/defs.d.ts` + } + }; +}; From 63ecccf3cfefed79f8f7bb2ec4ccea5a11f92240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Nison?= Date: Fri, 19 Apr 2019 12:26:01 +0100 Subject: [PATCH 5/8] Updates the README --- README.md | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 78f4264d..bcecc8c8 100644 --- a/README.md +++ b/README.md @@ -155,11 +155,24 @@ especially if there are other loaders/plugins involved in the compilation. **req * **typescript** `string`: If supplied this is a custom path where `typescript` can be found. Defaults to `require.resolve('typescript')`. -* **resolveModuleNameModule** `string`: -If supplied this is a path of a file where the worker can find a working implementation of `resolveModuleName` to use with TypeScript (exported through the `resolveModuleName` symbol). +* **resolveModuleNameModule** and **resolveTypeReferenceDirectiveModule** `string`: +Both of those options refer to files on the disk that respectively export a `resolveModuleName` or a `resolveTypeReferenceDirectiveModule` function. These functions will be used to resolve the import statements and the `` directives instead of the default TypeScript implementation. Check the following code for an example of what those functions should look like: +
+ Code sample -* **resolveTypeReferenceDirectiveModule** `string`: -If supplied this is a path of a file where the worker can find a working implementation of `resolveTypeReferenceDirective` to use with TypeScript (exported through the `resolveTypeReferenceDirective` symbol). +```js +const {resolveModuleName} = require(`ts-pnp`); + +exports.resolveModuleName = (typescript, moduleName, containingFile, compilerOptions, resolutionHost) => { + return resolveModuleName(moduleName, containingFile, compilerOptions, resolutionHost, typescript.resolveModuleName); +}; + +exports.resolveTypeReferenceDirective = (typescript, moduleName, containingFile, compilerOptions, resolutionHost) => { + return resolveModuleName(moduleName, containingFile, compilerOptions, resolutionHost, typescript.resolveTypeReferenceDirective); +}; +``` + +
### Pre-computed consts: * `ForkTsCheckerWebpackPlugin.ONE_CPU` - always use one CPU From 9536006ec8e11347c2b373787bf5dc7f06f88129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Nison?= Date: Fri, 19 Apr 2019 21:31:41 +0100 Subject: [PATCH 6/8] Fixes undefined env values on Node 8 --- src/index.ts | 48 ++++++++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/src/index.ts b/src/index.ts index c6e1723b..34028444 100644 --- a/src/index.ts +++ b/src/index.ts @@ -576,6 +576,34 @@ class ForkTsCheckerWebpackPlugin { } private spawnService() { + const env: any = { + ...process.env, + TYPESCRIPT_PATH: this.typescriptPath, + TSCONFIG: this.tsconfigPath, + COMPILER_OPTIONS: JSON.stringify(this.compilerOptions), + TSLINT: this.tslintPath || (this.tslint ? 'true' : ''), + CONTEXT: this.compiler.options.context, + TSLINTAUTOFIX: this.tslintAutoFix, + WATCH: this.isWatching ? this.watchPaths.join('|') : '', + WORK_DIVISION: Math.max(1, this.workersNumber), + MEMORY_LIMIT: this.memoryLimit, + CHECK_SYNTACTIC_ERRORS: this.checkSyntacticErrors, + USE_INCREMENTAL_API: this.useTypescriptIncrementalApi === true, + VUE: this.vue + }; + + if (typeof this.resolveModuleNameModule !== 'undefined') { + env.RESOLVE_MODULE_NAME = this.resolveModuleNameModule; + } else { + delete env.RESOLVE_MODULE_NAME; + } + + if (typeof this.resolveTypeReferenceDirectiveModule !== 'undefined') { + env.RESOLVE_TYPE_REFERENCE_DIRECTIVE = this.resolveTypeReferenceDirectiveModule; + } else { + delete env.RESOLVE_TYPE_REFERENCE_DIRECTIVE; + } + this.service = childProcess.fork( path.resolve( __dirname, @@ -583,31 +611,15 @@ class ForkTsCheckerWebpackPlugin { ), [], { + env, execArgv: (this.workersNumber > 1 ? [] : ['--max-old-space-size=' + this.memoryLimit] ).concat(this.nodeArgs), - env: { - ...process.env, - TYPESCRIPT_PATH: this.typescriptPath, - TSCONFIG: this.tsconfigPath, - COMPILER_OPTIONS: JSON.stringify(this.compilerOptions), - TSLINT: this.tslintPath || (this.tslint ? 'true' : ''), - CONTEXT: this.compiler.options.context, - TSLINTAUTOFIX: this.tslintAutoFix, - WATCH: this.isWatching ? this.watchPaths.join('|') : '', - WORK_DIVISION: Math.max(1, this.workersNumber), - MEMORY_LIMIT: this.memoryLimit, - CHECK_SYNTACTIC_ERRORS: this.checkSyntacticErrors, - USE_INCREMENTAL_API: this.useTypescriptIncrementalApi === true, - RESOLVE_MODULE_NAME: this.resolveModuleNameModule, - RESOLVE_TYPE_REFERENCE_DIRECTIVE: this - .resolveTypeReferenceDirectiveModule, - VUE: this.vue - }, stdio: ['inherit', 'inherit', 'inherit', 'ipc'] } ); + this.serviceRpc = new RpcProvider(message => this.service!.send(message)); this.service.on('message', message => this.serviceRpc!.dispatch(message)); From d03b29b41e7f1506013d70ac376d35ba226fb04b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Nison?= Date: Sat, 20 Apr 2019 06:34:28 +0100 Subject: [PATCH 7/8] Bumps version --- CHANGELOG.md | 10 +++++++--- package.json | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53b93489..31c437d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## v1.1.0 + +* [Add new custom resolution options](https://github.com/Realytics/fork-ts-checker-webpack-plugin/pull/250) + ## v1.0.4 * [gracefully handle error thrown from the service](https://github.com/Realytics/fork-ts-checker-webpack-plugin/pull/249) @@ -8,7 +12,7 @@ ## v1.0.2 -* [Fix ignoreLintWarning mark warnings as errors](https://github.com/Realytics/fork-ts-checker-webpack-plugin/pull/243) +* [Fix ignoreLintWarning mark warnings as errors](https://github.com/Realytics/fork-ts-checker-webpack-plugin/pull/243) ## v1.0.1 @@ -20,7 +24,7 @@ This is the first major version of `fork-ts-checker-webpack-plugin`. A long time coming :-) -There are actually no breaking changes that we're aware of; users of 0.x `fork-ts-checker-webpack-plugin` should be be able to upgrade without any drama. Users of TypeScript 3+ may notice a performance improvement as by default the plugin now uses the [incremental watch API](https://github.com/Microsoft/TypeScript/pull/20234) in TypeScript. Should this prove problematic you can opt out of using it by supplying `useTypescriptIncrementalApi: false`. +There are actually no breaking changes that we're aware of; users of 0.x `fork-ts-checker-webpack-plugin` should be be able to upgrade without any drama. Users of TypeScript 3+ may notice a performance improvement as by default the plugin now uses the [incremental watch API](https://github.com/Microsoft/TypeScript/pull/20234) in TypeScript. Should this prove problematic you can opt out of using it by supplying `useTypescriptIncrementalApi: false`. We are aware of an [issue with Vue and the incremental API](https://github.com/Realytics/fork-ts-checker-webpack-plugin/issues/219). We hope it will be fixed soon - a generous member of the community is taking a look. In the meantime, we will *not* default to using the incremental watch API when in Vue mode. @@ -79,7 +83,7 @@ Version `1.x` additionally supports webpack 5 alongside webpack 4, whose hooks a ```diff - compiler.hooks.forkTsCheckerDone.tap(...args) + const forkTsCheckerHooks = ForkTsCheckerWebpackPlugin.getCompilerHooks(compiler) -+ forkTsCheckerHooks.done.tap(...args) ++ forkTsCheckerHooks.done.tap(...args) ``` v1.0.0-alpha.0 drops support for node 6. diff --git a/package.json b/package.json index 4bd5f254..4dced347 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fork-ts-checker-webpack-plugin", - "version": "1.0.4", + "version": "1.1.0", "description": "Runs typescript type checker and linter on separate process.", "main": "lib/index.js", "types": "lib/index.d.ts", From 4d06c61a548f9d9a5f5e633ed6bc321feb8941d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Nison?= Date: Sat, 20 Apr 2019 06:38:07 +0100 Subject: [PATCH 8/8] Avoids any --- src/index.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index 34028444..6ed585af 100644 --- a/src/index.ts +++ b/src/index.ts @@ -576,20 +576,20 @@ class ForkTsCheckerWebpackPlugin { } private spawnService() { - const env: any = { + const env: { [key: string]: string | undefined } = { ...process.env, TYPESCRIPT_PATH: this.typescriptPath, TSCONFIG: this.tsconfigPath, COMPILER_OPTIONS: JSON.stringify(this.compilerOptions), TSLINT: this.tslintPath || (this.tslint ? 'true' : ''), CONTEXT: this.compiler.options.context, - TSLINTAUTOFIX: this.tslintAutoFix, + TSLINTAUTOFIX: String(this.tslintAutoFix), WATCH: this.isWatching ? this.watchPaths.join('|') : '', - WORK_DIVISION: Math.max(1, this.workersNumber), - MEMORY_LIMIT: this.memoryLimit, - CHECK_SYNTACTIC_ERRORS: this.checkSyntacticErrors, - USE_INCREMENTAL_API: this.useTypescriptIncrementalApi === true, - VUE: this.vue + WORK_DIVISION: String(Math.max(1, this.workersNumber)), + MEMORY_LIMIT: String(this.memoryLimit), + CHECK_SYNTACTIC_ERRORS: String(this.checkSyntacticErrors), + USE_INCREMENTAL_API: String(this.useTypescriptIncrementalApi === true), + VUE: String(this.vue) }; if (typeof this.resolveModuleNameModule !== 'undefined') {