Skip to content

Commit 1c2ab8c

Browse files
authored
fix: poll directories to prevent TS6307 (#454)
Webpack watcher notifies only about changes of files that are in the compilation dependencies graph. This means that it will not notice if we create a new file that is not missing. This is a difference between webpack and typescript watch mechanism that can lead to errors like TS6307. This fix polls directories content on getReport call and compare it with the previous content.
1 parent a3f09bc commit 1c2ab8c

File tree

3 files changed

+117
-24
lines changed

3 files changed

+117
-24
lines changed

src/typescript-reporter/reporter/ControlledTypeScriptSystem.ts

Lines changed: 103 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import * as ts from 'typescript';
2-
import { dirname } from 'path';
2+
import { dirname, join } from 'path';
33
import { createPassiveFileSystem } from '../file-system/PassiveFileSystem';
44
import forwardSlash from '../../utils/path/forwardSlash';
55
import { createRealFileSystem } from '../file-system/RealFileSystem';
66

77
interface ControlledTypeScriptSystem extends ts.System {
88
// control watcher
9+
invokeFileCreated(path: string): void;
910
invokeFileChanged(path: string): void;
1011
invokeFileDeleted(path: string): void;
12+
pollAndInvokeCreatedOrDeleted(): void;
1113
// control cache
1214
clearCache(): void;
1315
// mark these methods as defined - not optional
@@ -41,9 +43,10 @@ function createControlledTypeScriptSystem(
4143
mode: FileSystemMode = 'readonly'
4244
): ControlledTypeScriptSystem {
4345
// watchers
44-
const fileWatchersMap = new Map<string, ts.FileWatcherCallback[]>();
45-
const directoryWatchersMap = new Map<string, ts.DirectoryWatcherCallback[]>();
46-
const recursiveDirectoryWatchersMap = new Map<string, ts.DirectoryWatcherCallback[]>();
46+
const fileWatcherCallbacksMap = new Map<string, ts.FileWatcherCallback[]>();
47+
const directoryWatcherCallbacksMap = new Map<string, ts.DirectoryWatcherCallback[]>();
48+
const recursiveDirectoryWatcherCallbacksMap = new Map<string, ts.DirectoryWatcherCallback[]>();
49+
const directorySnapshots = new Map<string, string[]>();
4750
const deletedFiles = new Map<string, boolean>();
4851
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4952
const timeoutCallbacks = new Set<any>();
@@ -82,10 +85,12 @@ function createControlledTypeScriptSystem(
8285
function invokeFileWatchers(path: string, event: ts.FileWatcherEventKind) {
8386
const normalizedPath = realFileSystem.normalizePath(path);
8487

85-
const fileWatchers = fileWatchersMap.get(normalizedPath);
86-
if (fileWatchers) {
88+
const fileWatcherCallbacks = fileWatcherCallbacksMap.get(normalizedPath);
89+
if (fileWatcherCallbacks) {
8790
// typescript expects normalized paths with posix forward slash
88-
fileWatchers.forEach((fileWatcher) => fileWatcher(forwardSlash(normalizedPath), event));
91+
fileWatcherCallbacks.forEach((fileWatcherCallback) =>
92+
fileWatcherCallback(forwardSlash(normalizedPath), event)
93+
);
8994
}
9095
}
9196

@@ -97,24 +102,42 @@ function createControlledTypeScriptSystem(
97102
return;
98103
}
99104

100-
const directoryWatchers = directoryWatchersMap.get(directory);
101-
if (directoryWatchers) {
102-
directoryWatchers.forEach((directoryWatcher) =>
103-
directoryWatcher(forwardSlash(normalizedPath))
105+
const directoryWatcherCallbacks = directoryWatcherCallbacksMap.get(directory);
106+
if (directoryWatcherCallbacks) {
107+
directoryWatcherCallbacks.forEach((directoryWatcherCallback) =>
108+
directoryWatcherCallback(forwardSlash(normalizedPath))
104109
);
105110
}
106111

107-
recursiveDirectoryWatchersMap.forEach((recursiveDirectoryWatchers, watchedDirectory) => {
108-
if (
109-
watchedDirectory === directory ||
110-
(directory.startsWith(watchedDirectory) &&
111-
forwardSlash(directory)[watchedDirectory.length] === '/')
112-
) {
113-
recursiveDirectoryWatchers.forEach((recursiveDirectoryWatcher) =>
114-
recursiveDirectoryWatcher(forwardSlash(normalizedPath))
115-
);
112+
recursiveDirectoryWatcherCallbacksMap.forEach(
113+
(recursiveDirectoryWatcherCallbacks, watchedDirectory) => {
114+
if (
115+
watchedDirectory === directory ||
116+
(directory.startsWith(watchedDirectory) &&
117+
forwardSlash(directory)[watchedDirectory.length] === '/')
118+
) {
119+
recursiveDirectoryWatcherCallbacks.forEach((recursiveDirectoryWatcherCallback) =>
120+
recursiveDirectoryWatcherCallback(forwardSlash(normalizedPath))
121+
);
122+
}
116123
}
117-
});
124+
);
125+
}
126+
127+
function updateDirectorySnapshot(path: string, recursive = false) {
128+
const dirents = passiveFileSystem.readDir(path);
129+
130+
if (!directorySnapshots.has(path)) {
131+
directorySnapshots.set(
132+
path,
133+
dirents.filter((dirent) => dirent.isFile()).map((dirent) => join(path, dirent.name))
134+
);
135+
}
136+
if (recursive) {
137+
dirents
138+
.filter((dirent) => dirent.isDirectory())
139+
.forEach((dirent) => updateDirectorySnapshot(join(path, dirent.name)));
140+
}
118141
}
119142

120143
function getWriteFileSystem(path: string) {
@@ -180,15 +203,17 @@ function createControlledTypeScriptSystem(
180203
invokeFileWatchers(path, ts.FileWatcherEventKind.Changed);
181204
},
182205
watchFile(path: string, callback: ts.FileWatcherCallback): ts.FileWatcher {
183-
return createWatcher(fileWatchersMap, path, callback);
206+
return createWatcher(fileWatcherCallbacksMap, path, callback);
184207
},
185208
watchDirectory(
186209
path: string,
187210
callback: ts.DirectoryWatcherCallback,
188211
recursive = false
189212
): ts.FileWatcher {
213+
updateDirectorySnapshot(path, recursive);
214+
190215
return createWatcher(
191-
recursive ? recursiveDirectoryWatchersMap : directoryWatchersMap,
216+
recursive ? recursiveDirectoryWatcherCallbacksMap : directoryWatcherCallbacksMap,
192217
path,
193218
callback
194219
);
@@ -212,10 +237,18 @@ function createControlledTypeScriptSystem(
212237
await new Promise((resolve) => setImmediate(resolve));
213238
}
214239
},
240+
invokeFileCreated(path: string) {
241+
const normalizedPath = realFileSystem.normalizePath(path);
242+
243+
invokeFileWatchers(path, ts.FileWatcherEventKind.Created);
244+
invokeDirectoryWatchers(normalizedPath);
245+
246+
deletedFiles.set(normalizedPath, false);
247+
},
215248
invokeFileChanged(path: string) {
216249
const normalizedPath = realFileSystem.normalizePath(path);
217250

218-
if (deletedFiles.get(normalizedPath) || !fileWatchersMap.has(normalizedPath)) {
251+
if (deletedFiles.get(normalizedPath) || !fileWatcherCallbacksMap.has(normalizedPath)) {
219252
invokeFileWatchers(path, ts.FileWatcherEventKind.Created);
220253
invokeDirectoryWatchers(normalizedPath);
221254

@@ -234,6 +267,52 @@ function createControlledTypeScriptSystem(
234267
deletedFiles.set(normalizedPath, true);
235268
}
236269
},
270+
pollAndInvokeCreatedOrDeleted() {
271+
const prevDirectorySnapshots = new Map(directorySnapshots);
272+
273+
directorySnapshots.clear();
274+
directoryWatcherCallbacksMap.forEach((directoryWatcherCallback, path) => {
275+
updateDirectorySnapshot(path, false);
276+
});
277+
recursiveDirectoryWatcherCallbacksMap.forEach((recursiveDirectoryWatcherCallback, path) => {
278+
updateDirectorySnapshot(path, true);
279+
});
280+
281+
const filesCreated = new Set<string>();
282+
const filesDeleted = new Set<string>();
283+
284+
function diffDirectorySnapshots(
285+
prevFiles: string[] | undefined,
286+
nextFiles: string[] | undefined
287+
) {
288+
if (prevFiles && nextFiles) {
289+
nextFiles
290+
.filter((nextFile) => !prevFiles.includes(nextFile))
291+
.forEach((createdFile) => {
292+
filesCreated.add(createdFile);
293+
});
294+
prevFiles
295+
.filter((prevFile) => !nextFiles.includes(prevFile))
296+
.forEach((deletedFile) => {
297+
filesDeleted.add(deletedFile);
298+
});
299+
}
300+
}
301+
302+
prevDirectorySnapshots.forEach((prevFiles, path) =>
303+
diffDirectorySnapshots(prevFiles, directorySnapshots.get(path))
304+
);
305+
directorySnapshots.forEach((nextFiles, path) =>
306+
diffDirectorySnapshots(prevDirectorySnapshots.get(path), nextFiles)
307+
);
308+
309+
filesCreated.forEach((path) => {
310+
controlledSystem.invokeFileCreated(path);
311+
});
312+
filesDeleted.forEach((path) => {
313+
controlledSystem.invokeFileDeleted(path);
314+
});
315+
},
237316
clearCache() {
238317
passiveFileSystem.clearCache();
239318
realFileSystem.clearCache();

src/typescript-reporter/reporter/TypeScriptReporter.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,10 @@ function createTypeScriptReporter(configuration: TypeScriptReporterConfiguration
254254
}
255255
}
256256

257+
performance.markStart('Poll And Invoke Created Or Deleted');
258+
system.pollAndInvokeCreatedOrDeleted();
259+
performance.markEnd('Poll And Invoke Created Or Deleted');
260+
257261
changedFiles.forEach((changedFile) => {
258262
if (system) {
259263
system.invokeFileChanged(changedFile);

test/e2e/TypeScriptSolutionBuilderApi.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ describe('TypeScript SolutionBuilder API', () => {
101101
// this compilation should be successful
102102
await driver.waitForNoErrors();
103103

104+
await sandbox.write('packages/client/src/nested/additional.ts', 'export const x = 10;');
105+
await sandbox.patch(
106+
'packages/client/src/index.ts',
107+
'import { intersect, subtract } from "@project-references-fixture/shared";',
108+
'import { intersect, subtract } from "@project-references-fixture/shared";\nimport { x } from "./nested/additional";'
109+
);
110+
111+
// this compilation should be successful
112+
await driver.waitForNoErrors();
113+
104114
switch (mode) {
105115
case 'readonly':
106116
expect(await sandbox.exists('packages/shared/tsconfig.tsbuildinfo')).toEqual(false);

0 commit comments

Comments
 (0)