Skip to content

Commit 233f28c

Browse files
When directory watcher is invoked with any file from node_modules package, invalidate for file paths in that package (microsoft#43974)
Co-authored-by: Daniel Rosenwasser <[email protected]>
1 parent e0d7703 commit 233f28c

File tree

4 files changed

+625
-19
lines changed

4 files changed

+625
-19
lines changed

src/compiler/moduleNameResolver.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,7 +1150,7 @@ namespace ts {
11501150
}
11511151
const resolvedFromFile = loadModuleFromFile(extensions, candidate, onlyRecordFailures, state);
11521152
if (resolvedFromFile) {
1153-
const packageDirectory = considerPackageJson ? parseNodeModuleFromPath(resolvedFromFile) : undefined;
1153+
const packageDirectory = considerPackageJson ? parseNodeModuleFromPath(resolvedFromFile.path) : undefined;
11541154
const packageInfo = packageDirectory ? getPackageJsonInfo(packageDirectory, /*onlyRecordFailures*/ false, state) : undefined;
11551155
return withPackageId(packageInfo, resolvedFromFile);
11561156
}
@@ -1184,8 +1184,9 @@ namespace ts {
11841184
* For `/node_modules/@types/foo/bar/index.d.ts` this is packageDirectory: "@types/foo"
11851185
* For `/node_modules/foo/bar/index.d.ts` this is packageDirectory: "foo"
11861186
*/
1187-
function parseNodeModuleFromPath(resolved: PathAndExtension): string | undefined {
1188-
const path = normalizePath(resolved.path);
1187+
/* @internal */
1188+
export function parseNodeModuleFromPath(resolved: string): string | undefined {
1189+
const path = normalizePath(resolved);
11891190
const idx = path.lastIndexOf(nodeModulesPathPart);
11901191
if (idx === -1) {
11911192
return undefined;

src/compiler/resolutionCache.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,9 @@ namespace ts {
153153
const resolvedFileToResolution = createMultiMap<ResolutionWithFailedLookupLocations>();
154154

155155
let hasChangedAutomaticTypeDirectiveNames = false;
156-
const failedLookupChecks: Path[] = [];
157-
const startsWithPathChecks: Path[] = [];
158-
const isInDirectoryChecks: Path[] = [];
156+
let failedLookupChecks: Path[] | undefined;
157+
let startsWithPathChecks: Set<Path> | undefined;
158+
let isInDirectoryChecks: Path[] | undefined;
159159

160160
const getCurrentDirectory = memoize(() => resolutionHost.getCurrentDirectory!()); // TODO: GH#18217
161161
const cachedDirectoryStructureHost = resolutionHost.getCachedDirectoryStructureHost();
@@ -248,9 +248,9 @@ namespace ts {
248248
resolvedTypeReferenceDirectives.clear();
249249
resolvedFileToResolution.clear();
250250
resolutionsWithFailedLookups.length = 0;
251-
failedLookupChecks.length = 0;
252-
startsWithPathChecks.length = 0;
253-
isInDirectoryChecks.length = 0;
251+
failedLookupChecks = undefined;
252+
startsWithPathChecks = undefined;
253+
isInDirectoryChecks = undefined;
254254
// perDirectoryResolvedModuleNames and perDirectoryResolvedTypeReferenceDirectives could be non empty if there was exception during program update
255255
// (between startCachingPerDirectoryResolution and finishCachingPerDirectoryResolution)
256256
clearPerDirectoryResolutions();
@@ -766,7 +766,7 @@ namespace ts {
766766
if (isCreatingWatchedDirectory) {
767767
// Watching directory is created
768768
// Invalidate any resolution has failed lookup in this directory
769-
isInDirectoryChecks.push(fileOrDirectoryPath);
769+
(isInDirectoryChecks ||= []).push(fileOrDirectoryPath);
770770
}
771771
else {
772772
// If something to do with folder/file starting with "." in node_modules folder, skip it
@@ -785,8 +785,8 @@ namespace ts {
785785
if (isNodeModulesAtTypesDirectory(fileOrDirectoryPath) || isNodeModulesDirectory(fileOrDirectoryPath) ||
786786
isNodeModulesAtTypesDirectory(dirOfFileOrDirectory) || isNodeModulesDirectory(dirOfFileOrDirectory)) {
787787
// Invalidate any resolution from this directory
788-
failedLookupChecks.push(fileOrDirectoryPath);
789-
startsWithPathChecks.push(fileOrDirectoryPath);
788+
(failedLookupChecks ||= []).push(fileOrDirectoryPath);
789+
(startsWithPathChecks ||= new Set()).add(fileOrDirectoryPath);
790790
}
791791
else {
792792
if (!isPathWithDefaultFailedLookupExtension(fileOrDirectoryPath) && !customFailedLookupPaths.has(fileOrDirectoryPath)) {
@@ -797,30 +797,36 @@ namespace ts {
797797
return false;
798798
}
799799
// Resolution need to be invalidated if failed lookup location is same as the file or directory getting created
800-
failedLookupChecks.push(fileOrDirectoryPath);
800+
(failedLookupChecks ||= []).push(fileOrDirectoryPath);
801+
802+
// If the invalidated file is from a node_modules package, invalidate everything else
803+
// in the package since we might not get notifications for other files in the package.
804+
// This hardens our logic against unreliable file watchers.
805+
const packagePath = parseNodeModuleFromPath(fileOrDirectoryPath);
806+
if (packagePath) (startsWithPathChecks ||= new Set()).add(packagePath as Path);
801807
}
802808
}
803809
resolutionHost.scheduleInvalidateResolutionsOfFailedLookupLocations();
804810
}
805811

806812
function invalidateResolutionsOfFailedLookupLocations() {
807-
if (!failedLookupChecks.length && !startsWithPathChecks.length && !isInDirectoryChecks.length) {
813+
if (!failedLookupChecks && !startsWithPathChecks && !isInDirectoryChecks) {
808814
return false;
809815
}
810816

811817
const invalidated = invalidateResolutions(resolutionsWithFailedLookups, canInvalidateFailedLookupResolution);
812-
failedLookupChecks.length = 0;
813-
startsWithPathChecks.length = 0;
814-
isInDirectoryChecks.length = 0;
818+
failedLookupChecks = undefined;
819+
startsWithPathChecks = undefined;
820+
isInDirectoryChecks = undefined;
815821
return invalidated;
816822
}
817823

818824
function canInvalidateFailedLookupResolution(resolution: ResolutionWithFailedLookupLocations) {
819825
return resolution.failedLookupLocations.some(location => {
820826
const locationPath = resolutionHost.toPath(location);
821827
return contains(failedLookupChecks, locationPath) ||
822-
startsWithPathChecks.some(fileOrDirectoryPath => startsWith(locationPath, fileOrDirectoryPath)) ||
823-
isInDirectoryChecks.some(fileOrDirectoryPath => isInDirectoryPath(fileOrDirectoryPath, locationPath));
828+
firstDefinedIterator(startsWithPathChecks?.keys() || emptyIterator, fileOrDirectoryPath => startsWith(locationPath, fileOrDirectoryPath) ? true : undefined) ||
829+
isInDirectoryChecks?.some(fileOrDirectoryPath => isInDirectoryPath(fileOrDirectoryPath, locationPath));
824830
});
825831
}
826832

src/testRunner/unittests/tscWatch/resolutionCache.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,5 +449,84 @@ declare namespace myapp {
449449
},
450450
changes: emptyArray
451451
});
452+
453+
describe("works when installing something in node_modules or @types when there is no notification from fs for index file", () => {
454+
function getNodeAtTypes() {
455+
const nodeAtTypesIndex: File = {
456+
path: `${projectRoot}/node_modules/@types/node/index.d.ts`,
457+
content: `/// <reference path="base.d.ts" />`
458+
};
459+
const nodeAtTypesBase: File = {
460+
path: `${projectRoot}/node_modules/@types/node/base.d.ts`,
461+
content: `// Base definitions for all NodeJS modules that are not specific to any version of TypeScript:
462+
/// <reference path="ts3.6/base.d.ts" />`
463+
};
464+
const nodeAtTypes36Base: File = {
465+
path: `${projectRoot}/node_modules/@types/node/ts3.6/base.d.ts`,
466+
content: `/// <reference path="../globals.d.ts" />`
467+
};
468+
const nodeAtTypesGlobals: File = {
469+
path: `${projectRoot}/node_modules/@types/node/globals.d.ts`,
470+
content: `declare var process: NodeJS.Process;
471+
declare namespace NodeJS {
472+
interface Process {
473+
on(msg: string): void;
474+
}
475+
}`
476+
};
477+
return { nodeAtTypesIndex, nodeAtTypesBase, nodeAtTypes36Base, nodeAtTypesGlobals };
478+
}
479+
verifyTscWatch({
480+
scenario,
481+
subScenario: "works when installing something in node_modules or @types when there is no notification from fs for index file",
482+
commandLineArgs: ["--w", `--extendedDiagnostics`],
483+
sys: () => {
484+
const file: File = {
485+
path: `${projectRoot}/worker.ts`,
486+
content: `process.on("uncaughtException");`
487+
};
488+
const tsconfig: File = {
489+
path: `${projectRoot}/tsconfig.json`,
490+
content: "{}"
491+
};
492+
const { nodeAtTypesIndex, nodeAtTypesBase, nodeAtTypes36Base, nodeAtTypesGlobals } = getNodeAtTypes();
493+
return createWatchedSystem([file, libFile, tsconfig, nodeAtTypesIndex, nodeAtTypesBase, nodeAtTypes36Base, nodeAtTypesGlobals], { currentDirectory: projectRoot });
494+
},
495+
changes: [
496+
{
497+
caption: "npm ci step one: remove all node_modules files",
498+
change: sys => sys.deleteFolder(`${projectRoot}/node_modules/@types`, /*recursive*/ true),
499+
timeouts: runQueuedTimeoutCallbacks,
500+
},
501+
{
502+
caption: `npm ci step two: create atTypes but something else in the @types folder`,
503+
change: sys => sys.ensureFileOrFolder({
504+
path: `${projectRoot}/node_modules/@types/mocha/index.d.ts`,
505+
content: `export const foo = 10;`
506+
}),
507+
timeouts: runQueuedTimeoutCallbacks
508+
},
509+
{
510+
caption: `npm ci step three: create atTypes node folder`,
511+
change: sys => sys.ensureFileOrFolder({ path: `${projectRoot}/node_modules/@types/node` }),
512+
timeouts: runQueuedTimeoutCallbacks
513+
},
514+
{
515+
caption: `npm ci step four: create atTypes write all the files but dont invoke watcher for index.d.ts`,
516+
change: sys => {
517+
const { nodeAtTypesIndex, nodeAtTypesBase, nodeAtTypes36Base, nodeAtTypesGlobals } = getNodeAtTypes();
518+
sys.ensureFileOrFolder(nodeAtTypesBase);
519+
sys.ensureFileOrFolder(nodeAtTypesIndex, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true);
520+
sys.ensureFileOrFolder(nodeAtTypes36Base, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true);
521+
sys.ensureFileOrFolder(nodeAtTypesGlobals, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true);
522+
},
523+
timeouts: sys => {
524+
sys.runQueuedTimeoutCallbacks(); // update failed lookups
525+
sys.runQueuedTimeoutCallbacks(); // actual program update
526+
},
527+
},
528+
]
529+
});
530+
});
452531
});
453532
}

0 commit comments

Comments
 (0)