Skip to content

Commit

Permalink
fix: make custom watcher compatible with Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
piotr-oles committed Oct 10, 2020
1 parent 048816d commit a10d1e3
Show file tree
Hide file tree
Showing 12 changed files with 139 additions and 67 deletions.
2 changes: 1 addition & 1 deletion src/ForkTsCheckerWebpackPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/ForkTsCheckerWebpackPluginState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ interface ForkTsCheckerWebpackPluginState {
reportPromise: Promise<Report | undefined>;
issuesPromise: Promise<Issue[] | undefined>;
dependenciesPromise: Promise<Dependencies | undefined>;
lastDependencies: Dependencies | undefined;
removedFiles: string[];
watching: boolean;
initialized: boolean;
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/eslint-reporter/reporter/EsLintReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ function createEsLintReporter(configuration: EsLintReporterConfiguration): Repor
return {
files: [],
dirs: [],
extensions: [],
};
},
async getIssues() {
Expand Down
2 changes: 2 additions & 0 deletions src/hooks/tapAfterCompileToAddDependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ function tapAfterCompileToAddDependencies(
const dependencies = await state.dependenciesPromise;

if (dependencies) {
state.lastDependencies = dependencies;

dependencies.files.forEach((file) => {
compilation.fileDependencies.add(file);
});
Expand Down
163 changes: 101 additions & 62 deletions src/hooks/tapAfterEnvironmentToPatchWatching.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<string>,
Expand All @@ -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,
Expand All @@ -82,80 +143,58 @@ class InclusiveNodeWatchFileSystem implements WatchFileSystem {
callbackUndelayed
);

let paused = false;

// use custom watch for dirs
const dirWatchers = new Map<string, FSWatcher>();

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<string, number>();
};
// 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<string, number>();
};
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
);
}
});
Expand Down
1 change: 1 addition & 0 deletions src/reporter/Dependencies.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
interface Dependencies {
files: string[];
dirs: string[];
extensions: string[];
}

export { Dependencies };
11 changes: 8 additions & 3 deletions src/reporter/reporter-rpc/ReporterRpcClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: () =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ function createTypeScriptEmbeddedExtension({
},
};
},
extendSupportedFileExtensions(extensions: string[]) {
return [...extensions, ...embeddedExtensions];
},
};
}

Expand Down
1 change: 1 addition & 0 deletions src/typescript-reporter/extension/TypeScriptExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface TypeScriptHostExtension {
parsedCommandLine?: ts.ParsedCommandLine
): THost;
extendParseConfigFileHost?<THost extends ts.ParseConfigFileHost>(host: THost): THost;
extendSupportedFileExtensions?(extensions: string[]): string[];
}

interface TypeScriptReporterExtension {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ function createControlledTypeScriptSystem(
const deletedFiles = new Map<string, boolean>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const timeoutCallbacks = new Set<any>();
const caseSensitive = typescript.sys.useCaseSensitiveFileNames;
// always use case-sensitive as normalization to lower-case can be a problem for some
// third-party libraries, like fsevents
const caseSensitive = true;
const realFileSystem = createRealFileSystem(caseSensitive);
const passiveFileSystem = createPassiveFileSystem(caseSensitive, realFileSystem);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,18 @@ 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),
extensions: extensions,
};
}

Expand Down
7 changes: 7 additions & 0 deletions src/typescript-reporter/reporter/TypeScriptReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit a10d1e3

Please sign in to comment.