Skip to content

Commit 0232d4a

Browse files
authoredJun 16, 2020
Fixes issues with reload because of output emit (#39030)
* If there is no changes to folder structure when watching directories recursively, send the updates to fileNames only Fixes #37994 * Ignore excluded directories from wild card watching * Testcase showing that renaming file with non sync directory watcher displays correct error in the end Testcase for #38684
1 parent 540c219 commit 0232d4a

24 files changed

+2434
-140
lines changed
 

‎src/compiler/commandLineParser.ts

+34-1
Original file line numberDiff line numberDiff line change
@@ -2943,7 +2943,13 @@ namespace ts {
29432943
* @param extraFileExtensions optionaly file extra file extension information from host
29442944
*/
29452945
/* @internal */
2946-
export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: readonly FileExtensionInfo[] = []): ExpandResult {
2946+
export function getFileNamesFromConfigSpecs(
2947+
spec: ConfigFileSpecs,
2948+
basePath: string,
2949+
options: CompilerOptions,
2950+
host: ParseConfigHost,
2951+
extraFileExtensions: readonly FileExtensionInfo[] = emptyArray
2952+
): ExpandResult {
29472953
basePath = normalizePath(basePath);
29482954

29492955
const keyMapper = createGetCanonicalFileName(host.useCaseSensitiveFileNames);
@@ -3030,6 +3036,33 @@ namespace ts {
30303036
};
30313037
}
30323038

3039+
/* @internal */
3040+
export function isExcludedFile(
3041+
pathToCheck: string,
3042+
spec: ConfigFileSpecs,
3043+
basePath: string,
3044+
useCaseSensitiveFileNames: boolean,
3045+
currentDirectory: string
3046+
): boolean {
3047+
const { filesSpecs, validatedIncludeSpecs, validatedExcludeSpecs } = spec;
3048+
if (!length(validatedIncludeSpecs) || !length(validatedExcludeSpecs)) return false;
3049+
3050+
basePath = normalizePath(basePath);
3051+
3052+
const keyMapper = createGetCanonicalFileName(useCaseSensitiveFileNames);
3053+
if (filesSpecs) {
3054+
for (const fileName of filesSpecs) {
3055+
if (keyMapper(getNormalizedAbsolutePath(fileName, basePath)) === pathToCheck) return false;
3056+
}
3057+
}
3058+
3059+
const excludePattern = getRegularExpressionForWildcard(validatedExcludeSpecs, combinePaths(normalizePath(currentDirectory), basePath), "exclude");
3060+
const excludeRegex = excludePattern && getRegexFromPattern(excludePattern, useCaseSensitiveFileNames);
3061+
if (!excludeRegex) return false;
3062+
if (excludeRegex.test(pathToCheck)) return true;
3063+
return !hasExtension(pathToCheck) && excludeRegex.test(ensureTrailingDirectorySeparator(pathToCheck));
3064+
}
3065+
30333066
function validateSpecs(specs: readonly string[], errors: Push<Diagnostic>, allowTrailingRecursion: boolean, jsonSourceFile: TsConfigSourceFile | undefined, specKey: string): readonly string[] {
30343067
return specs.filter(spec => {
30353068
const diag = specToDiagnostic(spec, allowTrailingRecursion);

‎src/compiler/core.ts

+6
Original file line numberDiff line numberDiff line change
@@ -2049,17 +2049,20 @@ namespace ts {
20492049
let oldIndex = 0;
20502050
const newLen = newItems.length;
20512051
const oldLen = oldItems.length;
2052+
let hasChanges = false;
20522053
while (newIndex < newLen && oldIndex < oldLen) {
20532054
const newItem = newItems[newIndex];
20542055
const oldItem = oldItems[oldIndex];
20552056
const compareResult = comparer(newItem, oldItem);
20562057
if (compareResult === Comparison.LessThan) {
20572058
inserted(newItem);
20582059
newIndex++;
2060+
hasChanges = true;
20592061
}
20602062
else if (compareResult === Comparison.GreaterThan) {
20612063
deleted(oldItem);
20622064
oldIndex++;
2065+
hasChanges = true;
20632066
}
20642067
else {
20652068
unchanged(oldItem, newItem);
@@ -2069,10 +2072,13 @@ namespace ts {
20692072
}
20702073
while (newIndex < newLen) {
20712074
inserted(newItems[newIndex++]);
2075+
hasChanges = true;
20722076
}
20732077
while (oldIndex < oldLen) {
20742078
deleted(oldItems[oldIndex++]);
2079+
hasChanges = true;
20752080
}
2081+
return hasChanges;
20762082
}
20772083

20782084
export function fill<T>(length: number, cb: (index: number) => T): T[] {

‎src/compiler/sys.ts

+48-30
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@ namespace ts {
475475

476476
const cache = createMap<HostDirectoryWatcher>();
477477
const callbackCache = createMultiMap<{ dirName: string; callback: DirectoryWatcherCallback; }>();
478-
const cacheToUpdateChildWatches = createMap<{ dirName: string; options: WatchOptions | undefined; }>();
478+
const cacheToUpdateChildWatches = createMap<{ dirName: string; options: WatchOptions | undefined; fileNames: string[]; }>();
479479
let timerToUpdateChildWatches: any;
480480

481481
const filePathComparer = getStringComparer(!host.useCaseSensitiveFileNames);
@@ -538,9 +538,12 @@ namespace ts {
538538
};
539539
}
540540

541-
function invokeCallbacks(dirPath: Path, fileNameOrInvokeMap: string | Map<true>) {
541+
type InvokeMap = Map<string[] | true>;
542+
function invokeCallbacks(dirPath: Path, fileName: string): void;
543+
function invokeCallbacks(dirPath: Path, invokeMap: InvokeMap, fileNames: string[] | undefined): void;
544+
function invokeCallbacks(dirPath: Path, fileNameOrInvokeMap: string | InvokeMap, fileNames?: string[]) {
542545
let fileName: string | undefined;
543-
let invokeMap: Map<true> | undefined;
546+
let invokeMap: InvokeMap | undefined;
544547
if (isString(fileNameOrInvokeMap)) {
545548
fileName = fileNameOrInvokeMap;
546549
}
@@ -549,10 +552,21 @@ namespace ts {
549552
}
550553
// Call the actual callback
551554
callbackCache.forEach((callbacks, rootDirName) => {
552-
if (invokeMap && invokeMap.has(rootDirName)) return;
555+
if (invokeMap && invokeMap.get(rootDirName) === true) return;
553556
if (rootDirName === dirPath || (startsWith(dirPath, rootDirName) && dirPath[rootDirName.length] === directorySeparator)) {
554557
if (invokeMap) {
555-
invokeMap.set(rootDirName, true);
558+
if (fileNames) {
559+
const existing = invokeMap.get(rootDirName);
560+
if (existing) {
561+
(existing as string[]).push(...fileNames);
562+
}
563+
else {
564+
invokeMap.set(rootDirName, fileNames.slice());
565+
}
566+
}
567+
else {
568+
invokeMap.set(rootDirName, true);
569+
}
556570
}
557571
else {
558572
callbacks.forEach(({ callback }) => callback(fileName!));
@@ -566,7 +580,7 @@ namespace ts {
566580
const parentWatcher = cache.get(dirPath);
567581
if (parentWatcher && host.directoryExists(dirName)) {
568582
// Schedule the update and postpone invoke for callbacks
569-
scheduleUpdateChildWatches(dirName, dirPath, options);
583+
scheduleUpdateChildWatches(dirName, dirPath, fileName, options);
570584
return;
571585
}
572586

@@ -575,9 +589,13 @@ namespace ts {
575589
removeChildWatches(parentWatcher);
576590
}
577591

578-
function scheduleUpdateChildWatches(dirName: string, dirPath: Path, options: WatchOptions | undefined) {
579-
if (!cacheToUpdateChildWatches.has(dirPath)) {
580-
cacheToUpdateChildWatches.set(dirPath, { dirName, options });
592+
function scheduleUpdateChildWatches(dirName: string, dirPath: Path, fileName: string, options: WatchOptions | undefined) {
593+
const existing = cacheToUpdateChildWatches.get(dirPath);
594+
if (existing) {
595+
existing.fileNames.push(fileName);
596+
}
597+
else {
598+
cacheToUpdateChildWatches.set(dirPath, { dirName, options, fileNames: [fileName] });
581599
}
582600
if (timerToUpdateChildWatches) {
583601
host.clearTimeout(timerToUpdateChildWatches);
@@ -590,22 +608,30 @@ namespace ts {
590608
timerToUpdateChildWatches = undefined;
591609
sysLog(`sysLog:: onTimerToUpdateChildWatches:: ${cacheToUpdateChildWatches.size}`);
592610
const start = timestamp();
593-
const invokeMap = createMap<true>();
611+
const invokeMap = createMap<string[]>();
594612

595613
while (!timerToUpdateChildWatches && cacheToUpdateChildWatches.size) {
596-
const { value: [dirPath, { dirName, options }], done } = cacheToUpdateChildWatches.entries().next();
614+
const { value: [dirPath, { dirName, options, fileNames }], done } = cacheToUpdateChildWatches.entries().next();
597615
Debug.assert(!done);
598616
cacheToUpdateChildWatches.delete(dirPath);
599617
// Because the child refresh is fresh, we would need to invalidate whole root directory being watched
600618
// to ensure that all the changes are reflected at this time
601-
invokeCallbacks(dirPath as Path, invokeMap);
602-
updateChildWatches(dirName, dirPath as Path, options);
619+
const hasChanges = updateChildWatches(dirName, dirPath as Path, options);
620+
invokeCallbacks(dirPath as Path, invokeMap, hasChanges ? undefined : fileNames);
603621
}
604622

605623
sysLog(`sysLog:: invokingWatchers:: ${timestamp() - start}ms:: ${cacheToUpdateChildWatches.size}`);
606624
callbackCache.forEach((callbacks, rootDirName) => {
607-
if (invokeMap.has(rootDirName)) {
608-
callbacks.forEach(({ callback, dirName }) => callback(dirName));
625+
const existing = invokeMap.get(rootDirName);
626+
if (existing) {
627+
callbacks.forEach(({ callback, dirName }) => {
628+
if (isArray(existing)) {
629+
existing.forEach(callback);
630+
}
631+
else {
632+
callback(dirName);
633+
}
634+
});
609635
}
610636
});
611637

@@ -623,34 +649,26 @@ namespace ts {
623649
}
624650
}
625651

626-
function updateChildWatches(dirName: string, dirPath: Path, options: WatchOptions | undefined) {
652+
function updateChildWatches(parentDir: string, parentDirPath: Path, options: WatchOptions | undefined) {
627653
// Iterate through existing children and update the watches if needed
628-
const parentWatcher = cache.get(dirPath);
629-
if (parentWatcher) {
630-
parentWatcher.childWatches = watchChildDirectories(dirName, parentWatcher.childWatches, options);
631-
}
632-
}
633-
634-
/**
635-
* Watch the directories in the parentDir
636-
*/
637-
function watchChildDirectories(parentDir: string, existingChildWatches: ChildWatches, options: WatchOptions | undefined): ChildWatches {
654+
const parentWatcher = cache.get(parentDirPath);
655+
if (!parentWatcher) return false;
638656
let newChildWatches: ChildDirectoryWatcher[] | undefined;
639-
enumerateInsertsAndDeletes<string, ChildDirectoryWatcher>(
657+
const hasChanges = enumerateInsertsAndDeletes<string, ChildDirectoryWatcher>(
640658
host.directoryExists(parentDir) ? mapDefined(host.getAccessibleSortedChildDirectories(parentDir), child => {
641659
const childFullName = getNormalizedAbsolutePath(child, parentDir);
642660
// Filter our the symbolic link directories since those arent included in recursive watch
643661
// which is same behaviour when recursive: true is passed to fs.watch
644662
return !isIgnoredPath(childFullName) && filePathComparer(childFullName, normalizePath(host.realpath(childFullName))) === Comparison.EqualTo ? childFullName : undefined;
645663
}) : emptyArray,
646-
existingChildWatches,
664+
parentWatcher.childWatches,
647665
(child, childWatcher) => filePathComparer(child, childWatcher.dirName),
648666
createAndAddChildDirectoryWatcher,
649667
closeFileWatcher,
650668
addChildDirectoryWatcher
651669
);
652-
653-
return newChildWatches || emptyArray;
670+
parentWatcher.childWatches = newChildWatches || emptyArray;
671+
return hasChanges;
654672

655673
/**
656674
* Create new childDirectoryWatcher and add it to the new ChildDirectoryWatcher list

‎src/compiler/tsbuildPublic.ts

+19-50
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,9 @@ namespace ts {
853853
getConfigFileParsingDiagnostics(config),
854854
config.projectReferences
855855
);
856+
if (state.watch) {
857+
state.builderPrograms.set(projectPath, program);
858+
}
856859
step++;
857860
}
858861

@@ -982,7 +985,7 @@ namespace ts {
982985
if (emitResult.emittedFiles && state.writeFileName) {
983986
emitResult.emittedFiles.forEach(name => listEmittedFile(state, config, name));
984987
}
985-
afterProgramDone(state, projectPath, program, config);
988+
afterProgramDone(state, program, config);
986989
step = BuildStep.QueueReferencingProjects;
987990
return emitResult;
988991
}
@@ -1023,7 +1026,7 @@ namespace ts {
10231026
newestDeclarationFileContentChangedTime,
10241027
oldestOutputFileName
10251028
});
1026-
afterProgramDone(state, projectPath, program, config);
1029+
afterProgramDone(state, program, config);
10271030
step = BuildStep.QueueReferencingProjects;
10281031
buildResult = resultFlags;
10291032
return emitDiagnostics;
@@ -1269,7 +1272,6 @@ namespace ts {
12691272

12701273
function afterProgramDone<T extends BuilderProgram>(
12711274
state: SolutionBuilderState<T>,
1272-
proj: ResolvedConfigFilePath,
12731275
program: T | undefined,
12741276
config: ParsedCommandLine
12751277
) {
@@ -1278,10 +1280,7 @@ namespace ts {
12781280
if (state.host.afterProgramEmitAndDiagnostics) {
12791281
state.host.afterProgramEmitAndDiagnostics(program);
12801282
}
1281-
if (state.watch) {
1282-
program.releaseProgram();
1283-
state.builderPrograms.set(proj, program);
1284-
}
1283+
program.releaseProgram();
12851284
}
12861285
else if (state.host.afterEmitBundle) {
12871286
state.host.afterEmitBundle(config);
@@ -1304,7 +1303,7 @@ namespace ts {
13041303
// List files if any other build error using program (emit errors already report files)
13051304
state.projectStatus.set(resolvedPath, { type: UpToDateStatusType.Unbuildable, reason: `${errorType} errors` });
13061305
if (canEmitBuildInfo) return { buildResult, step: BuildStep.EmitBuildInfo };
1307-
afterProgramDone(state, resolvedPath, program, config);
1306+
afterProgramDone(state, program, config);
13081307
return { buildResult, step: BuildStep.QueueReferencingProjects };
13091308
}
13101309

@@ -1809,38 +1808,6 @@ namespace ts {
18091808
));
18101809
}
18111810

1812-
function isSameFile(state: SolutionBuilderState, file1: string, file2: string) {
1813-
return comparePaths(file1, file2, state.currentDirectory, !state.host.useCaseSensitiveFileNames()) === Comparison.EqualTo;
1814-
}
1815-
1816-
function isOutputFile(state: SolutionBuilderState, fileName: string, configFile: ParsedCommandLine) {
1817-
if (configFile.options.noEmit) return false;
1818-
1819-
// ts or tsx files are not output
1820-
if (!fileExtensionIs(fileName, Extension.Dts) &&
1821-
(fileExtensionIs(fileName, Extension.Ts) || fileExtensionIs(fileName, Extension.Tsx))) {
1822-
return false;
1823-
}
1824-
1825-
// If options have --outFile or --out, check if its that
1826-
const out = outFile(configFile.options);
1827-
if (out && (isSameFile(state, fileName, out) || isSameFile(state, fileName, removeFileExtension(out) + Extension.Dts))) {
1828-
return true;
1829-
}
1830-
1831-
// If declarationDir is specified, return if its a file in that directory
1832-
if (configFile.options.declarationDir && containsPath(configFile.options.declarationDir, fileName, state.currentDirectory, !state.host.useCaseSensitiveFileNames())) {
1833-
return true;
1834-
}
1835-
1836-
// If --outDir, check if file is in that directory
1837-
if (configFile.options.outDir && containsPath(configFile.options.outDir, fileName, state.currentDirectory, !state.host.useCaseSensitiveFileNames())) {
1838-
return true;
1839-
}
1840-
1841-
return !forEach(configFile.fileNames, inputFile => isSameFile(state, fileName, inputFile));
1842-
}
1843-
18441811
function watchWildCardDirectories(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) {
18451812
if (!state.watch) return;
18461813
updateWatchingWildcardDirectories(
@@ -1850,16 +1817,18 @@ namespace ts {
18501817
state.hostWithWatch,
18511818
dir,
18521819
fileOrDirectory => {
1853-
const fileOrDirectoryPath = toPath(state, fileOrDirectory);
1854-
if (fileOrDirectoryPath !== toPath(state, dir) && hasExtension(fileOrDirectoryPath) && !isSupportedSourceFileName(fileOrDirectory, parsed.options)) {
1855-
state.writeLog(`Project: ${resolved} Detected file add/remove of non supported extension: ${fileOrDirectory}`);
1856-
return;
1857-
}
1858-
1859-
if (isOutputFile(state, fileOrDirectory, parsed)) {
1860-
state.writeLog(`${fileOrDirectory} is output file`);
1861-
return;
1862-
}
1820+
if (isIgnoredFileFromWildCardWatching({
1821+
watchedDirPath: toPath(state, dir),
1822+
fileOrDirectory,
1823+
fileOrDirectoryPath: toPath(state, fileOrDirectory),
1824+
configFileName: resolved,
1825+
configFileSpecs: parsed.configFileSpecs!,
1826+
currentDirectory: state.currentDirectory,
1827+
options: parsed.options,
1828+
program: state.builderPrograms.get(resolvedPath),
1829+
useCaseSensitiveFileNames: state.parseConfigFileHost.useCaseSensitiveFileNames,
1830+
writeLog: s => state.writeLog(s)
1831+
})) return;
18631832

18641833
invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.Partial);
18651834
},

‎src/compiler/watchPublic.ts

+13-10
Original file line numberDiff line numberDiff line change
@@ -734,23 +734,26 @@ namespace ts {
734734
fileOrDirectory => {
735735
Debug.assert(!!configFileName);
736736

737-
let fileOrDirectoryPath: Path | undefined = toPath(fileOrDirectory);
737+
const fileOrDirectoryPath = toPath(fileOrDirectory);
738738

739739
// Since the file existence changed, update the sourceFiles cache
740740
if (cachedDirectoryStructureHost) {
741741
cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath);
742742
}
743743
nextSourceFileVersion(fileOrDirectoryPath);
744744

745-
fileOrDirectoryPath = removeIgnoredPath(fileOrDirectoryPath);
746-
if (!fileOrDirectoryPath) return;
747-
748-
// If the the added or created file or directory is not supported file name, ignore the file
749-
// But when watched directory is added/removed, we need to reload the file list
750-
if (fileOrDirectoryPath !== directory && hasExtension(fileOrDirectoryPath) && !isSupportedSourceFileName(fileOrDirectory, compilerOptions)) {
751-
writeLog(`Project: ${configFileName} Detected file add/remove of non supported extension: ${fileOrDirectory}`);
752-
return;
753-
}
745+
if (isIgnoredFileFromWildCardWatching({
746+
watchedDirPath: toPath(directory),
747+
fileOrDirectory,
748+
fileOrDirectoryPath,
749+
configFileName,
750+
configFileSpecs,
751+
options: compilerOptions,
752+
program: getCurrentBuilderProgram(),
753+
currentDirectory,
754+
useCaseSensitiveFileNames,
755+
writeLog
756+
})) return;
754757

755758
// Reload is pending, do the reload
756759
if (reloadLevel !== ConfigFileProgramReloadLevel.Full) {

0 commit comments

Comments
 (0)