Skip to content

Commit 528bf84

Browse files
committed
Create a test case for module resolution when symlinked folder contents change and resolve modules
Test case for microsoft#22349
1 parent 5ecfa78 commit 528bf84

File tree

2 files changed

+205
-5
lines changed

2 files changed

+205
-5
lines changed

src/harness/unittests/tsserverProjectSystem.ts

+180
Original file line numberDiff line numberDiff line change
@@ -7706,6 +7706,186 @@ namespace ts.projectSystem {
77067706
}
77077707
}
77087708
});
7709+
7710+
describe("module resolution when symlinked folder contents change and resolve modules", () => {
7711+
const projectRootPath = "/users/username/projects/myproject";
7712+
const packages = `${projectRootPath}/javascript/packages`;
7713+
const recognizersDateTime = `${packages}/recognizers-date-time`;
7714+
const recognizersText = `${packages}/recognizers-text`;
7715+
const recognizersTextDist = `${recognizersText}/dist`;
7716+
const moduleName = "@microsoft/recognizers-text";
7717+
const moduleNameInFile = `"${moduleName}"`;
7718+
const recognizersDateTimeSrcFile: File = {
7719+
path: `${recognizersDateTime}/src/datetime/baseDate.ts`,
7720+
content: `import {C} from ${moduleNameInFile};
7721+
new C();`
7722+
};
7723+
const recognizerDateTimeTsconfigPath = `${recognizersDateTime}/tsconfig.json`;
7724+
const recognizerDateTimeTsconfigWithoutPathMapping: File = {
7725+
path: recognizerDateTimeTsconfigPath,
7726+
content: JSON.stringify({
7727+
include: ["src"]
7728+
})
7729+
};
7730+
const recognizerDateTimeTsconfigWithPathMapping: File = {
7731+
path: recognizerDateTimeTsconfigPath,
7732+
content: JSON.stringify({
7733+
compilerOptions: {
7734+
rootDir: "src",
7735+
baseUrl: "./",
7736+
paths: {
7737+
"@microsoft/*": ["../*"]
7738+
}
7739+
},
7740+
include: ["src"]
7741+
})
7742+
};
7743+
const nodeModulesRecorgnizersText: SymLink = {
7744+
path: `${recognizersDateTime}/node_modules/@microsoft/recognizers-text`,
7745+
symLink: recognizersText
7746+
};
7747+
const recognizerTextSrcFile: File = {
7748+
path: `${recognizersText}/src/recognizers-text.ts`,
7749+
content: `export class C { method () { return 10; } }`
7750+
};
7751+
const recongnizerTextDistTypingFile: File = {
7752+
path: `${recognizersTextDist}/types/recognizers-text.d.ts`,
7753+
content: `export class C { method(): number; }`
7754+
};
7755+
const recongnizerTextPackageJson: File = {
7756+
path: `${recognizersText}/package.json`,
7757+
content: JSON.stringify({
7758+
typings: "dist/types/recognizers-text.d.ts"
7759+
})
7760+
};
7761+
const filesInProjectWithUnresolvedModule = [recognizerDateTimeTsconfigPath, libFile.path, recognizersDateTimeSrcFile.path];
7762+
const filesInProjectWithResolvedModule = [...filesInProjectWithUnresolvedModule, recongnizerTextDistTypingFile.path];
7763+
7764+
function verifyErrors(session: TestSession, semanticErrors: protocol.Diagnostic[]) {
7765+
session.clearMessages();
7766+
const expectedSequenceId = session.getNextSeq();
7767+
session.executeCommandSeq<protocol.GeterrRequest>({
7768+
command: server.CommandNames.Geterr,
7769+
arguments: {
7770+
delay: 0,
7771+
files: [recognizersDateTimeSrcFile.path],
7772+
}
7773+
});
7774+
7775+
const host = session.host;
7776+
host.checkTimeoutQueueLengthAndRun(1);
7777+
7778+
checkErrorMessage(session, "syntaxDiag", { file: recognizersDateTimeSrcFile.path, diagnostics: [] });
7779+
session.clearMessages();
7780+
7781+
host.runQueuedImmediateCallbacks(1);
7782+
7783+
checkErrorMessage(session, "semanticDiag", { file: recognizersDateTimeSrcFile.path, diagnostics: semanticErrors });
7784+
session.clearMessages();
7785+
7786+
host.runQueuedImmediateCallbacks(1);
7787+
7788+
checkErrorMessage(session, "suggestionDiag", {
7789+
file: recognizersDateTimeSrcFile.path,
7790+
diagnostics: [],
7791+
});
7792+
checkCompleteEvent(session, 2, expectedSequenceId);
7793+
}
7794+
7795+
function createSingleWatchMap(paths: string[]) {
7796+
return arrayToMap(paths, p => p, () => 1);
7797+
}
7798+
7799+
function verifyWatchedFilesAndDirectories(host: TestServerHost, files: string[], directories: string[]) {
7800+
checkWatchedFilesDetailed(host, createSingleWatchMap(files.filter(f => f !== recognizersDateTimeSrcFile.path)));
7801+
checkWatchedDirectories(host, emptyArray, /*recursive*/ false);
7802+
checkWatchedDirectoriesDetailed(host, createSingleWatchMap(directories), /*recursive*/ true);
7803+
}
7804+
7805+
function createSessionAndOpenFile(host: TestServerHost) {
7806+
const session = createSession(host, { canUseEvents: true });
7807+
session.executeCommandSeq<protocol.OpenRequest>({
7808+
command: protocol.CommandTypes.Open,
7809+
arguments: {
7810+
file: recognizersDateTimeSrcFile.path,
7811+
projectRootPath
7812+
}
7813+
});
7814+
return session;
7815+
}
7816+
7817+
function verifyModuleResolution(withPathMapping: boolean) {
7818+
describe(withPathMapping ? "when tsconfig file contains path mapping" : "when tsconfig does not contain path mapping", () => {
7819+
const filesWithSources = [libFile, recognizersDateTimeSrcFile, withPathMapping ? recognizerDateTimeTsconfigWithPathMapping : recognizerDateTimeTsconfigWithoutPathMapping, recognizerTextSrcFile, recongnizerTextPackageJson];
7820+
const filesWithNodeModulesSetup = [...filesWithSources, nodeModulesRecorgnizersText];
7821+
const filesAfterCompilation = [...filesWithNodeModulesSetup, recongnizerTextDistTypingFile];
7822+
7823+
const watchedDirectoriesWithResolvedModule = [`${recognizersDateTime}/src`, withPathMapping ? packages : recognizersDateTime, ...getTypeRootsFromLocation(recognizersDateTime)];
7824+
const watchedDirectoriesWithUnresolvedModule = [recognizersDateTime, ...watchedDirectoriesWithResolvedModule, ...getNodeModuleDirectories(packages)];
7825+
7826+
function verifyProjectWithResolvedModule(session: TestSession) {
7827+
const projectService = session.getProjectService();
7828+
const project = projectService.configuredProjects.get(recognizerDateTimeTsconfigPath);
7829+
checkProjectActualFiles(project, filesInProjectWithResolvedModule);
7830+
verifyWatchedFilesAndDirectories(session.host, filesInProjectWithResolvedModule, watchedDirectoriesWithResolvedModule);
7831+
verifyErrors(session, []);
7832+
}
7833+
7834+
function verifyProjectWithUnresolvedModule(session: TestSession) {
7835+
const projectService = session.getProjectService();
7836+
const project = projectService.configuredProjects.get(recognizerDateTimeTsconfigPath);
7837+
checkProjectActualFiles(project, filesInProjectWithUnresolvedModule);
7838+
verifyWatchedFilesAndDirectories(session.host, filesInProjectWithUnresolvedModule, watchedDirectoriesWithUnresolvedModule);
7839+
const startOffset = recognizersDateTimeSrcFile.content.indexOf('"') + 1;
7840+
verifyErrors(session, [
7841+
createDiagnostic({ line: 1, offset: startOffset }, { line: 1, offset: startOffset + moduleNameInFile.length }, Diagnostics.Cannot_find_module_0, [moduleName])
7842+
]);
7843+
}
7844+
7845+
it("when project compiles from sources", () => {
7846+
const host = createServerHost(filesWithSources);
7847+
const session = createSessionAndOpenFile(host);
7848+
verifyProjectWithUnresolvedModule(session);
7849+
7850+
host.reloadFS(filesAfterCompilation);
7851+
host.runQueuedTimeoutCallbacks();
7852+
7853+
verifyProjectWithResolvedModule(session);
7854+
});
7855+
7856+
it("when project has node_modules setup but doesnt have modules in typings folder and then recompiles", () => {
7857+
const host = createServerHost(filesWithNodeModulesSetup);
7858+
const session = createSessionAndOpenFile(host);
7859+
verifyProjectWithUnresolvedModule(session);
7860+
7861+
host.reloadFS(filesAfterCompilation);
7862+
host.runQueuedTimeoutCallbacks();
7863+
7864+
verifyProjectWithResolvedModule(session);
7865+
});
7866+
7867+
it("when project recompiles after deleting generated folders", () => {
7868+
const host = createServerHost(filesAfterCompilation);
7869+
const session = createSessionAndOpenFile(host);
7870+
7871+
verifyProjectWithResolvedModule(session);
7872+
7873+
host.removeFolder(recognizersTextDist, /*recursive*/ true);
7874+
host.runQueuedTimeoutCallbacks();
7875+
7876+
verifyProjectWithUnresolvedModule(session);
7877+
7878+
host.ensureFileOrFolder(recongnizerTextDistTypingFile);
7879+
host.runQueuedTimeoutCallbacks();
7880+
7881+
verifyProjectWithResolvedModule(session);
7882+
});
7883+
});
7884+
}
7885+
7886+
verifyModuleResolution(/*withPathMapping*/ false);
7887+
verifyModuleResolution(/*withPathMapping*/ true);
7888+
});
77097889
});
77107890

77117891
describe("tsserverProjectSystem forceConsistentCasingInFileNames", () => {

src/harness/virtualFileSystemWithWatch.ts

+25-5
Original file line numberDiff line numberDiff line change
@@ -93,25 +93,27 @@ interface Array<T> {}`
9393
return isString((<SymLink>fileOrFolderOrSymLink).symLink);
9494
}
9595

96-
interface FSEntry {
96+
interface FSEntryBase {
9797
path: Path;
9898
fullPath: string;
9999
modifiedTime: Date;
100100
}
101101

102-
interface FsFile extends FSEntry {
102+
interface FsFile extends FSEntryBase {
103103
content: string;
104104
fileSize?: number;
105105
}
106106

107-
interface FsFolder extends FSEntry {
107+
interface FsFolder extends FSEntryBase {
108108
entries: SortedArray<FSEntry>;
109109
}
110110

111-
interface FsSymLink extends FSEntry {
111+
interface FsSymLink extends FSEntryBase {
112112
symLink: string;
113113
}
114114

115+
type FSEntry = FsFile | FsFolder | FsSymLink;
116+
115117
function isFsFolder(s: FSEntry): s is FsFolder {
116118
return s && isArray((<FsFolder>s).entries);
117119
}
@@ -599,6 +601,24 @@ interface Array<T> {}`
599601
}
600602
}
601603

604+
removeFolder(folderPath: string, recursive?: boolean) {
605+
const path = this.toFullPath(folderPath);
606+
const currentEntry = this.fs.get(path) as FsFolder;
607+
Debug.assert(isFsFolder(currentEntry));
608+
if (recursive && currentEntry.entries.length) {
609+
const subEntries = currentEntry.entries.slice();
610+
subEntries.forEach(fsEntry => {
611+
if (isFsFolder(fsEntry)) {
612+
this.removeFolder(fsEntry.fullPath, recursive);
613+
}
614+
else {
615+
this.removeFileOrFolder(fsEntry, returnFalse);
616+
}
617+
});
618+
}
619+
this.removeFileOrFolder(currentEntry, returnFalse);
620+
}
621+
602622
// For overriding the methods
603623
invokeWatchedDirectoriesCallback(folderFullPath: string, relativePath: string) {
604624
invokeWatcherCallbacks(this.watchedDirectories.get(this.toPath(folderFullPath)), cb => this.directoryCallback(cb, relativePath));
@@ -643,7 +663,7 @@ interface Array<T> {}`
643663
}
644664
}
645665

646-
private toFsEntry(path: string): FSEntry {
666+
private toFsEntry(path: string): FSEntryBase {
647667
const fullPath = getNormalizedAbsolutePath(path, this.currentDirectory);
648668
return {
649669
path: this.toPath(fullPath),

0 commit comments

Comments
 (0)