diff --git a/src/ForkTsCheckerWebpackPlugin.ts b/src/ForkTsCheckerWebpackPlugin.ts index ff3b4735..d7c9ff3b 100644 --- a/src/ForkTsCheckerWebpackPlugin.ts +++ b/src/ForkTsCheckerWebpackPlugin.ts @@ -63,7 +63,7 @@ class ForkTsCheckerWebpackPlugin implements webpack.Plugin { if (reporters.length) { const reporter = createAggregatedReporter(composeReporterRpcClients(reporters)); - tapAfterEnvironmentToPatchWatching(compiler); + tapAfterEnvironmentToPatchWatching(compiler, state); tapStartToConnectAndRunReporter(compiler, reporter, configuration, state); tapDoneToCollectRemoved(compiler, configuration, state); tapAfterCompileToAddDependencies(compiler, configuration, state); diff --git a/src/ForkTsCheckerWebpackPluginState.ts b/src/ForkTsCheckerWebpackPluginState.ts index 2d91d648..6448fccb 100644 --- a/src/ForkTsCheckerWebpackPluginState.ts +++ b/src/ForkTsCheckerWebpackPluginState.ts @@ -6,6 +6,7 @@ interface ForkTsCheckerWebpackPluginState { reportPromise: Promise; issuesPromise: Promise; dependenciesPromise: Promise; + lastDependencies: Dependencies | undefined; removedFiles: string[]; watching: boolean; initialized: boolean; @@ -17,6 +18,7 @@ function createForkTsCheckerWebpackPluginState(): ForkTsCheckerWebpackPluginStat reportPromise: Promise.resolve(undefined), issuesPromise: Promise.resolve(undefined), dependenciesPromise: Promise.resolve(undefined), + lastDependencies: undefined, removedFiles: [], watching: false, initialized: false, diff --git a/src/eslint-reporter/reporter/EsLintReporter.ts b/src/eslint-reporter/reporter/EsLintReporter.ts index 29526691..514c749b 100644 --- a/src/eslint-reporter/reporter/EsLintReporter.ts +++ b/src/eslint-reporter/reporter/EsLintReporter.ts @@ -21,6 +21,7 @@ function createEsLintReporter(configuration: EsLintReporterConfiguration): Repor return { files: [], dirs: [], + extensions: [], }; }, async getIssues() { diff --git a/src/hooks/tapAfterCompileToAddDependencies.ts b/src/hooks/tapAfterCompileToAddDependencies.ts index d05b283a..51357f7f 100644 --- a/src/hooks/tapAfterCompileToAddDependencies.ts +++ b/src/hooks/tapAfterCompileToAddDependencies.ts @@ -16,6 +16,8 @@ function tapAfterCompileToAddDependencies( const dependencies = await state.dependenciesPromise; if (dependencies) { + state.lastDependencies = dependencies; + dependencies.files.forEach((file) => { compilation.fileDependencies.add(file); }); diff --git a/src/hooks/tapAfterEnvironmentToPatchWatching.ts b/src/hooks/tapAfterEnvironmentToPatchWatching.ts index 7662772f..c98770d4 100644 --- a/src/hooks/tapAfterEnvironmentToPatchWatching.ts +++ b/src/hooks/tapAfterEnvironmentToPatchWatching.ts @@ -1,7 +1,8 @@ import webpack from 'webpack'; import chokidar, { FSWatcher } from 'chokidar'; import { EventEmitter } from 'events'; -import { realpathSync } from 'fs'; +import { extname } from 'path'; +import { ForkTsCheckerWebpackPluginState } from '../ForkTsCheckerWebpackPluginState'; // webpack 4 interface interface WatcherV4 { @@ -60,7 +61,15 @@ class InclusiveNodeWatchFileSystem implements WatchFileSystem { return this.watchFileSystem.watcher || this.watchFileSystem.wfs?.watcher; } - constructor(private watchFileSystem: WatchFileSystem) {} + constructor( + private watchFileSystem: WatchFileSystem, + private pluginState: ForkTsCheckerWebpackPluginState + ) {} + + private paused = true; + private fileWatcher: Watcher | undefined; + private dirsWatcher: FSWatcher | undefined; + private dirsWatched: string[] = []; watch( files: Iterable, @@ -71,8 +80,60 @@ class InclusiveNodeWatchFileSystem implements WatchFileSystem { callback?: Function, callbackUndelayed?: Function ): Watcher { + if (!this.dirsWatcher) { + const interval = typeof options?.poll === 'number' ? options.poll : undefined; + + this.dirsWatcher = chokidar.watch([], { + ignoreInitial: true, + ignorePermissionErrors: true, + ignored: ['**/node_modules/**', '**/.git/**'], + usePolling: options?.poll ? true : undefined, + interval: interval, + binaryInterval: interval, + alwaysStat: true, + atomic: true, + awaitWriteFinish: true, + }); + + this.dirsWatcher.on('add', (file, stats) => { + if (this.paused) { + return; + } + + const extension = extname(file); + const supportedExtensions = this.pluginState.lastDependencies?.extensions || []; + + if (!supportedExtensions.includes(extension)) { + return; + } + + const mtime = stats?.mtimeMs || stats?.ctimeMs || 1; + + this.watcher?._onChange(file, mtime, file, 'rename'); + }); + this.dirsWatcher.on('unlink', (file) => { + if (this.paused) { + return; + } + + const extension = extname(file); + const supportedExtensions = this.pluginState.lastDependencies?.extensions || []; + + if (!supportedExtensions.includes(extension)) { + return; + } + + this.watcher?._onRemove(file, file, 'rename'); + }); + } + + // cleanup old standard watchers + if (this.fileWatcher) { + this.fileWatcher.close(); + } + // use standard watch file system for files and missing - const standardWatcher = this.watchFileSystem.watch( + this.fileWatcher = this.watchFileSystem.watch( files, [], missing, @@ -82,80 +143,58 @@ class InclusiveNodeWatchFileSystem implements WatchFileSystem { callbackUndelayed ); - let paused = false; - - // use custom watch for dirs - const dirWatchers = new Map(); - - for (const dir of dirs) { - if (!dirWatchers.has(dir)) { - const watcher = chokidar.watch(dir, { - ignored: ['**/node_modules/**', '**/.git/**'], - ignoreInitial: true, - alwaysStat: true, - }); - watcher.on('add', (file, stats) => { - if (paused) { - return; - } - const path = realpathSync.native(file); - - this.watcher?._onChange(path, stats?.mtimeMs || 0, path, 'rename'); - }); - watcher.on('unlink', (file) => { - if (paused) { - return; - } - - this.watcher?._onRemove(file, file, 'rename'); - }); - dirWatchers.set(dir, watcher); - } - } + // calculate what to change + const prevDirs = this.dirsWatched; + const nextDirs = Array.from(dirs); + const dirsToUnwatch = prevDirs.filter((prevDir) => !nextDirs.includes(prevDir)); + const dirsToWatch = nextDirs.filter((nextDir) => !prevDirs.includes(nextDir)); - const getFileTimeInfoEntries = () => { - if ((standardWatcher as WatcherV4).getFileTimestamps) { - return (standardWatcher as WatcherV4).getFileTimestamps(); - } else if ((standardWatcher as WatcherV5).getFileTimeInfoEntries) { - return (standardWatcher as WatcherV5).getFileTimeInfoEntries(); - } - return new Map(); - }; + // update dirs watcher + if (dirsToUnwatch.length) { + this.dirsWatcher.unwatch(dirsToUnwatch); + } + if (dirsToWatch.length) { + this.dirsWatcher.add(dirsToWatch); + } - const getContextTimeInfoEntries = () => { - if ((standardWatcher as WatcherV4).getContextTimestamps) { - return (standardWatcher as WatcherV4).getContextTimestamps(); - } else if ((standardWatcher as WatcherV5).getContextTimeInfoEntries) { - return (standardWatcher as WatcherV5).getContextTimeInfoEntries(); - } - return new Map(); - }; + this.paused = false; + this.dirsWatched = nextDirs; return { - close() { - standardWatcher.close(); - dirWatchers.forEach((dirWatcher) => dirWatcher.close()); - paused = true; + ...this.fileWatcher, + close: () => { + if (this.fileWatcher) { + this.fileWatcher.close(); + this.fileWatcher = undefined; + } + if (this.dirsWatcher) { + this.dirsWatcher.close(); + this.dirsWatcher = undefined; + } + + this.paused = true; }, - pause() { - standardWatcher.pause(); - paused = true; + pause: () => { + if (this.fileWatcher) { + this.fileWatcher.pause(); + } + this.paused = true; }, - getFileTimestamps: getFileTimeInfoEntries, - getContextTimestamps: getContextTimeInfoEntries, - getFileTimeInfoEntries: getFileTimeInfoEntries, - getContextTimeInfoEntries: getContextTimeInfoEntries, }; } } -function tapAfterEnvironmentToPatchWatching(compiler: webpack.Compiler) { +function tapAfterEnvironmentToPatchWatching( + compiler: webpack.Compiler, + state: ForkTsCheckerWebpackPluginState +) { compiler.hooks.afterEnvironment.tap('ForkTsCheckerWebpackPlugin', () => { const watchFileSystem = (compiler as CompilerWithWatchFileSystem).watchFileSystem; if (watchFileSystem) { // wrap original watch file system (compiler as CompilerWithWatchFileSystem).watchFileSystem = new InclusiveNodeWatchFileSystem( - watchFileSystem + watchFileSystem, + state ); } }); diff --git a/src/reporter/Dependencies.ts b/src/reporter/Dependencies.ts index a79db310..a18b845f 100644 --- a/src/reporter/Dependencies.ts +++ b/src/reporter/Dependencies.ts @@ -1,6 +1,7 @@ interface Dependencies { files: string[]; dirs: string[]; + extensions: string[]; } export { Dependencies }; diff --git a/src/reporter/reporter-rpc/ReporterRpcClient.ts b/src/reporter/reporter-rpc/ReporterRpcClient.ts index dbee98f5..fc054fe9 100644 --- a/src/reporter/reporter-rpc/ReporterRpcClient.ts +++ b/src/reporter/reporter-rpc/ReporterRpcClient.ts @@ -71,10 +71,15 @@ function composeReporterRpcClients(clients: ReporterRpcClient[]): ReporterRpcCli Promise.all(reports.map((report) => report.getDependencies())).then((dependencies) => dependencies.reduce( (mergedDependencies, singleDependencies) => ({ - files: [...mergedDependencies.files, ...singleDependencies.files], - dirs: [...mergedDependencies.dirs, ...singleDependencies.dirs], + files: Array.from( + new Set([...mergedDependencies.files, ...singleDependencies.files]) + ), + dirs: Array.from(new Set([...mergedDependencies.dirs, ...singleDependencies.dirs])), + extensions: Array.from( + new Set([...mergedDependencies.extensions, ...singleDependencies.extensions]) + ), }), - { files: [], dirs: [] } + { files: [], dirs: [], extensions: [] } ) ), getIssues: () => diff --git a/src/typescript-reporter/extension/TypeScriptEmbeddedExtension.ts b/src/typescript-reporter/extension/TypeScriptEmbeddedExtension.ts index 08ec267c..a0738e3f 100644 --- a/src/typescript-reporter/extension/TypeScriptEmbeddedExtension.ts +++ b/src/typescript-reporter/extension/TypeScriptEmbeddedExtension.ts @@ -167,6 +167,9 @@ function createTypeScriptEmbeddedExtension({ }, }; }, + extendSupportedFileExtensions(extensions: string[]) { + return [...extensions, ...embeddedExtensions]; + }, }; } diff --git a/src/typescript-reporter/extension/TypeScriptExtension.ts b/src/typescript-reporter/extension/TypeScriptExtension.ts index cb450762..3b274fae 100644 --- a/src/typescript-reporter/extension/TypeScriptExtension.ts +++ b/src/typescript-reporter/extension/TypeScriptExtension.ts @@ -21,6 +21,7 @@ interface TypeScriptHostExtension { parsedCommandLine?: ts.ParsedCommandLine ): THost; extendParseConfigFileHost?(host: THost): THost; + extendSupportedFileExtensions?(extensions: string[]): string[]; } interface TypeScriptReporterExtension { diff --git a/src/typescript-reporter/reporter/TypeScriptConfigurationParser.ts b/src/typescript-reporter/reporter/TypeScriptConfigurationParser.ts index 8ed68597..ea5058a2 100644 --- a/src/typescript-reporter/reporter/TypeScriptConfigurationParser.ts +++ b/src/typescript-reporter/reporter/TypeScriptConfigurationParser.ts @@ -1,6 +1,7 @@ import * as ts from 'typescript'; import { TypeScriptConfigurationOverwrite } from '../TypeScriptConfigurationOverwrite'; import { Dependencies } from '../../reporter'; +import { existsSync, realpathSync } from 'fs'; function parseTypeScriptConfiguration( typescript: typeof ts, @@ -82,9 +83,22 @@ function getDependenciesFromTypeScriptConfiguration( }); } + const extensions = [ + typescript.Extension.Ts, + typescript.Extension.Tsx, + typescript.Extension.Js, + typescript.Extension.Jsx, + typescript.Extension.TsBuildInfo, + ]; + return { - files: Array.from(files), - dirs: Array.from(dirs), + files: Array.from(files) + .filter((file) => existsSync(file)) + .map((file) => realpathSync.native(file)), + dirs: Array.from(dirs) + .filter((dir) => existsSync(dir)) + .map((dir) => realpathSync.native(dir)), + extensions: extensions, }; } diff --git a/src/typescript-reporter/reporter/TypeScriptReporter.ts b/src/typescript-reporter/reporter/TypeScriptReporter.ts index 7ce8698f..871e2d4d 100644 --- a/src/typescript-reporter/reporter/TypeScriptReporter.ts +++ b/src/typescript-reporter/reporter/TypeScriptReporter.ts @@ -215,6 +215,13 @@ function createTypeScriptReporter(configuration: TypeScriptReporterConfiguration async getDependencies() { if (!dependencies) { dependencies = getDependencies(); + for (const extension of extensions) { + if (extension.extendSupportedFileExtensions) { + dependencies.extensions = extension.extendSupportedFileExtensions( + dependencies.extensions + ); + } + } } return dependencies;