From 7b9b0f8da71ded5a005f581f16b2d768c49ddd4b Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 7 Dec 2018 10:30:10 -0800 Subject: [PATCH] Split resolutionCache and watchApi tests into its own unittest --- src/testRunner/tsconfig.json | 8 +- src/testRunner/unittests/resolutionCache.ts | 981 +++++++++++++++ src/testRunner/unittests/tsbuildWatchMode.ts | 1 - src/testRunner/unittests/tscWatchHelpers.ts | 237 ++++ src/testRunner/unittests/tscWatchMode.ts | 645 ---------- src/testRunner/unittests/tsserverHelpers.ts | 679 +++++++++++ .../unittests/tsserverProjectSystem.ts | 1065 ----------------- src/testRunner/unittests/typingsInstaller.ts | 1 - src/testRunner/unittests/watchApi.ts | 40 + src/testRunner/unittests/watchEnvironment.ts | 50 + 10 files changed, 1993 insertions(+), 1714 deletions(-) create mode 100644 src/testRunner/unittests/resolutionCache.ts create mode 100644 src/testRunner/unittests/tscWatchHelpers.ts create mode 100644 src/testRunner/unittests/tsserverHelpers.ts create mode 100644 src/testRunner/unittests/watchApi.ts diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index bd3eded500..674188eee6 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -37,8 +37,8 @@ "runner.ts", "unittests/extractTestHelpers.ts", - "unittests/tsserverProjectSystem.ts", - "unittests/typingsInstaller.ts", + "unittests/tscWatchHelpers.ts", + "unittests/tsserverHelpers.ts", "unittests/asserts.ts", "unittests/base64.ts", @@ -72,6 +72,7 @@ "unittests/projectErrors.ts", "unittests/projectReferences.ts", "unittests/publicApi.ts", + "unittests/resolutionCache.ts", "unittests/reuseProgramStructure.ts", "unittests/session.ts", "unittests/semver.ts", @@ -86,8 +87,11 @@ "unittests/tsbuildWatchMode.ts", "unittests/tsconfigParsing.ts", "unittests/tscWatchMode.ts", + "unittests/tsserverProjectSystem.ts", + "unittests/typingsInstaller.ts", "unittests/versionCache.ts", "unittests/watchEnvironment.ts", + "unittests/watchApi.ts", "unittests/evaluation/asyncArrow.ts", "unittests/evaluation/asyncGenerator.ts", "unittests/evaluation/forAwaitOf.ts", diff --git a/src/testRunner/unittests/resolutionCache.ts b/src/testRunner/unittests/resolutionCache.ts new file mode 100644 index 0000000000..fbc792481f --- /dev/null +++ b/src/testRunner/unittests/resolutionCache.ts @@ -0,0 +1,981 @@ +namespace ts.tscWatch { + describe("resolutionCache:: tsc-watch module resolution caching", () => { + it("works", () => { + const root = { + path: "/a/d/f0.ts", + content: `import {x} from "f1"` + }; + const imported = { + path: "/a/f1.ts", + content: `foo()` + }; + + const files = [root, imported, libFile]; + const host = createWatchedSystem(files); + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); + + const f1IsNotModule = getDiagnosticOfFileFromProgram(watch(), root.path, root.content.indexOf('"f1"'), '"f1"'.length, Diagnostics.File_0_is_not_a_module, imported.path); + const cannotFindFoo = getDiagnosticOfFileFromProgram(watch(), imported.path, imported.content.indexOf("foo"), "foo".length, Diagnostics.Cannot_find_name_0, "foo"); + + // ensure that imported file was found + checkOutputErrorsInitial(host, [f1IsNotModule, cannotFindFoo]); + + const originalFileExists = host.fileExists; + { + const newContent = `import {x} from "f1" + var x: string = 1;`; + root.content = newContent; + host.reloadFS(files); + + // patch fileExists to make sure that disk is not touched + host.fileExists = notImplemented; + + // trigger synchronization to make sure that import will be fetched from the cache + host.runQueuedTimeoutCallbacks(); + + // ensure file has correct number of errors after edit + checkOutputErrorsIncremental(host, [ + f1IsNotModule, + getDiagnosticOfFileFromProgram(watch(), root.path, newContent.indexOf("var x") + "var ".length, "x".length, Diagnostics.Type_0_is_not_assignable_to_type_1, 1, "string"), + cannotFindFoo + ]); + } + { + let fileExistsIsCalled = false; + host.fileExists = (fileName): boolean => { + if (fileName === "lib.d.ts") { + return false; + } + fileExistsIsCalled = true; + assert.isTrue(fileName.indexOf("/f2.") !== -1); + return originalFileExists.call(host, fileName); + }; + + root.content = `import {x} from "f2"`; + host.reloadFS(files); + + // trigger synchronization to make sure that LSHost will try to find 'f2' module on disk + host.runQueuedTimeoutCallbacks(); + + // ensure file has correct number of errors after edit + checkOutputErrorsIncremental(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "f2") + ]); + + assert.isTrue(fileExistsIsCalled); + } + { + let fileExistsCalled = false; + host.fileExists = (fileName): boolean => { + if (fileName === "lib.d.ts") { + return false; + } + fileExistsCalled = true; + assert.isTrue(fileName.indexOf("/f1.") !== -1); + return originalFileExists.call(host, fileName); + }; + + const newContent = `import {x} from "f1"`; + root.content = newContent; + + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + + checkOutputErrorsIncremental(host, [f1IsNotModule, cannotFindFoo]); + assert.isTrue(fileExistsCalled); + } + }); + + it("loads missing files from disk", () => { + const root = { + path: `/a/foo.ts`, + content: `import {x} from "bar"` + }; + + const imported = { + path: `/a/bar.d.ts`, + content: `export const y = 1;` + }; + + const files = [root, libFile]; + const host = createWatchedSystem(files); + const originalFileExists = host.fileExists; + + let fileExistsCalledForBar = false; + host.fileExists = fileName => { + if (fileName === "lib.d.ts") { + return false; + } + if (!fileExistsCalledForBar) { + fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; + } + + return originalFileExists.call(host, fileName); + }; + + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); + + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); + checkOutputErrorsInitial(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") + ]); + + fileExistsCalledForBar = false; + root.content = `import {y} from "bar"`; + host.reloadFS(files.concat(imported)); + + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, emptyArray); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); + }); + + it("should compile correctly when resolved module goes missing and then comes back (module is not part of the root)", () => { + const root = { + path: `/a/foo.ts`, + content: `import {x} from "bar"` + }; + + const imported = { + path: `/a/bar.d.ts`, + content: `export const y = 1;export const x = 10;` + }; + + const files = [root, libFile]; + const filesWithImported = files.concat(imported); + const host = createWatchedSystem(filesWithImported); + const originalFileExists = host.fileExists; + let fileExistsCalledForBar = false; + host.fileExists = fileName => { + if (fileName === "lib.d.ts") { + return false; + } + if (!fileExistsCalledForBar) { + fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; + } + return originalFileExists.call(host, fileName); + }; + + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); + + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); + checkOutputErrorsInitial(host, emptyArray); + + fileExistsCalledForBar = false; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); + checkOutputErrorsIncremental(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") + ]); + + fileExistsCalledForBar = false; + host.reloadFS(filesWithImported); + host.checkTimeoutQueueLengthAndRun(1); + checkOutputErrorsIncremental(host, emptyArray); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); + }); + + it("works when module resolution changes to ambient module", () => { + const root = { + path: "/a/b/foo.ts", + content: `import * as fs from "fs";` + }; + + const packageJson = { + path: "/a/b/node_modules/@types/node/package.json", + content: ` +{ + "main": "" +} +` + }; + + const nodeType = { + path: "/a/b/node_modules/@types/node/index.d.ts", + content: ` +declare module "fs" { + export interface Stats { + isFile(): boolean; + } +}` + }; + + const files = [root, libFile]; + const filesWithNodeType = files.concat(packageJson, nodeType); + const host = createWatchedSystem(files, { currentDirectory: "/a/b" }); + + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { }); + + checkOutputErrorsInitial(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") + ]); + + host.reloadFS(filesWithNodeType); + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, emptyArray); + }); + + it("works when included file with ambient module changes", () => { + const root = { + path: "/a/b/foo.ts", + content: ` +import * as fs from "fs"; +import * as u from "url"; +` + }; + + const file = { + path: "/a/b/bar.d.ts", + content: ` +declare module "url" { + export interface Url { + href?: string; + } +} +` + }; + + const fileContentWithFS = ` +declare module "fs" { + export interface Stats { + isFile(): boolean; + } +} +`; + + const files = [root, file, libFile]; + const host = createWatchedSystem(files, { currentDirectory: "/a/b" }); + + const watch = createWatchOfFilesAndCompilerOptions([root.path, file.path], host, {}); + + checkOutputErrorsInitial(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") + ]); + + file.content += fileContentWithFS; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, emptyArray); + }); + + it("works when reusing program with files from external library", () => { + interface ExpectedFile { path: string; isExpectedToEmit?: boolean; content?: string; } + const configDir = "/a/b/projects/myProject/src/"; + const file1: File = { + path: configDir + "file1.ts", + content: 'import module1 = require("module1");\nmodule1("hello");' + }; + const file2: File = { + path: configDir + "file2.ts", + content: 'import module11 = require("module1");\nmodule11("hello");' + }; + const module1: File = { + path: "/a/b/projects/myProject/node_modules/module1/index.js", + content: "module.exports = options => { return options.toString(); }" + }; + const configFile: File = { + path: configDir + "tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + allowJs: true, + rootDir: ".", + outDir: "../dist", + moduleResolution: "node", + maxNodeModuleJsDepth: 1 + } + }) + }; + const outDirFolder = "/a/b/projects/myProject/dist/"; + const programFiles = [file1, file2, module1, libFile]; + const host = createWatchedSystem(programFiles.concat(configFile), { currentDirectory: "/a/b/projects/myProject/" }); + const watch = createWatchOfConfigFile(configFile.path, host); + checkProgramActualFiles(watch(), programFiles.map(f => f.path)); + checkOutputErrorsInitial(host, emptyArray); + const expectedFiles: ExpectedFile[] = [ + createExpectedEmittedFile(file1), + createExpectedEmittedFile(file2), + createExpectedToNotEmitFile("index.js"), + createExpectedToNotEmitFile("src/index.js"), + createExpectedToNotEmitFile("src/file1.js"), + createExpectedToNotEmitFile("src/file2.js"), + createExpectedToNotEmitFile("lib.js"), + createExpectedToNotEmitFile("lib.d.ts") + ]; + verifyExpectedFiles(expectedFiles); + + file1.content += "\n;"; + expectedFiles[0].content += ";\n"; // Only emit file1 with this change + expectedFiles[1].isExpectedToEmit = false; + host.reloadFS(programFiles.concat(configFile)); + host.runQueuedTimeoutCallbacks(); + checkProgramActualFiles(watch(), programFiles.map(f => f.path)); + checkOutputErrorsIncremental(host, emptyArray); + verifyExpectedFiles(expectedFiles); + + + function verifyExpectedFiles(expectedFiles: ExpectedFile[]) { + forEach(expectedFiles, f => { + assert.equal(!!host.fileExists(f.path), f.isExpectedToEmit, "File " + f.path + " is expected to " + (f.isExpectedToEmit ? "emit" : "not emit")); + if (f.isExpectedToEmit) { + assert.equal(host.readFile(f.path), f.content, "Expected contents of " + f.path); + } + }); + } + + function createExpectedToNotEmitFile(fileName: string): ExpectedFile { + return { + path: outDirFolder + fileName, + isExpectedToEmit: false + }; + } + + function createExpectedEmittedFile(file: File): ExpectedFile { + return { + path: removeFileExtension(file.path.replace(configDir, outDirFolder)) + Extension.Js, + isExpectedToEmit: true, + content: '"use strict";\nexports.__esModule = true;\n' + file.content.replace("import", "var") + "\n" + }; + } + }); + + it("works when renaming node_modules folder that already contains @types folder", () => { + const currentDirectory = "/user/username/projects/myproject"; + const file: File = { + path: `${currentDirectory}/a.ts`, + content: `import * as q from "qqq";` + }; + const module: File = { + path: `${currentDirectory}/node_modules2/@types/qqq/index.d.ts`, + content: "export {}" + }; + const files = [file, module, libFile]; + const host = createWatchedSystem(files, { currentDirectory }); + const watch = createWatchOfFilesAndCompilerOptions([file.path], host); + + checkProgramActualFiles(watch(), [file.path, libFile.path]); + checkOutputErrorsInitial(host, [getDiagnosticModuleNotFoundOfFile(watch(), file, "qqq")]); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectories(host, [`${currentDirectory}/node_modules`, `${currentDirectory}/node_modules/@types`], /*recursive*/ true); + + host.renameFolder(`${currentDirectory}/node_modules2`, `${currentDirectory}/node_modules`); + host.runQueuedTimeoutCallbacks(); + checkProgramActualFiles(watch(), [file.path, libFile.path, `${currentDirectory}/node_modules/@types/qqq/index.d.ts`]); + checkOutputErrorsIncremental(host, emptyArray); + }); + + describe("ignores files/folder changes in node_modules that start with '.'", () => { + const projectPath = "/user/username/projects/project"; + const npmCacheFile: File = { + path: `${projectPath}/node_modules/.cache/babel-loader/89c02171edab901b9926470ba6d5677e.ts`, + content: JSON.stringify({ something: 10 }) + }; + const file1: File = { + path: `${projectPath}/test.ts`, + content: `import { x } from "somemodule";` + }; + const file2: File = { + path: `${projectPath}/node_modules/somemodule/index.d.ts`, + content: `export const x = 10;` + }; + const files = [libFile, file1, file2]; + const expectedFiles = files.map(f => f.path); + it("when watching node_modules in inferred project for failed lookup", () => { + const host = createWatchedSystem(files); + const watch = createWatchOfFilesAndCompilerOptions([file1.path], host, {}, /*maxNumberOfFilesToIterateForInvalidation*/ 1); + checkProgramActualFiles(watch(), expectedFiles); + host.checkTimeoutQueueLength(0); + + host.ensureFileOrFolder(npmCacheFile); + host.checkTimeoutQueueLength(0); + }); + it("when watching node_modules as part of wild card directories in config project", () => { + const config: File = { + path: `${projectPath}/tsconfig.json`, + content: "{}" + }; + const host = createWatchedSystem(files.concat(config)); + const watch = createWatchOfConfigFile(config.path, host); + checkProgramActualFiles(watch(), expectedFiles); + host.checkTimeoutQueueLength(0); + + host.ensureFileOrFolder(npmCacheFile); + host.checkTimeoutQueueLength(0); + }); + }); + }); + + describe("resolutionCache:: tsc-watch with modules linked to sibling folder", () => { + const projectRoot = "/user/username/projects/project"; + const mainPackageRoot = `${projectRoot}/main`; + const linkedPackageRoot = `${projectRoot}/linked-package`; + const mainFile: File = { + path: `${mainPackageRoot}/index.ts`, + content: "import { Foo } from '@scoped/linked-package'" + }; + const config: File = { + path: `${mainPackageRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { module: "commonjs", moduleResolution: "node", baseUrl: ".", rootDir: "." }, + files: ["index.ts"] + }) + }; + const linkedPackageInMain: SymLink = { + path: `${mainPackageRoot}/node_modules/@scoped/linked-package`, + symLink: `${linkedPackageRoot}` + }; + const linkedPackageJson: File = { + path: `${linkedPackageRoot}/package.json`, + content: JSON.stringify({ name: "@scoped/linked-package", version: "0.0.1", types: "dist/index.d.ts", main: "dist/index.js" }) + }; + const linkedPackageIndex: File = { + path: `${linkedPackageRoot}/dist/index.d.ts`, + content: "export * from './other';" + }; + const linkedPackageOther: File = { + path: `${linkedPackageRoot}/dist/other.d.ts`, + content: 'export declare const Foo = "BAR";' + }; + + it("verify watched directories", () => { + const files = [libFile, mainFile, config, linkedPackageInMain, linkedPackageJson, linkedPackageIndex, linkedPackageOther]; + const host = createWatchedSystem(files, { currentDirectory: mainPackageRoot }); + createWatchOfConfigFile("tsconfig.json", host); + checkWatchedFilesDetailed(host, [libFile.path, mainFile.path, config.path, linkedPackageIndex.path, linkedPackageOther.path], 1); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectoriesDetailed(host, [`${mainPackageRoot}/@scoped`, `${mainPackageRoot}/node_modules`, linkedPackageRoot, `${mainPackageRoot}/node_modules/@types`, `${projectRoot}/node_modules/@types`], 1, /*recursive*/ true); + }); + }); +} + +namespace ts.projectSystem { + function createHostModuleResolutionTrace(host: TestServerHost & ModuleResolutionHost) { + const resolutionTrace: string[] = []; + host.trace = resolutionTrace.push.bind(resolutionTrace); + return resolutionTrace; + } + + describe("resolutionCache:: tsserverProjectSystem extra resolution pass in lshost", () => { + it("can load typings that are proper modules", () => { + const file1 = { + path: "/a/b/app.js", + content: `var x = require("lib")` + }; + const lib = { + path: "/a/cache/node_modules/@types/lib/index.d.ts", + content: "export let x = 1" + }; + const host: TestServerHost & ModuleResolutionHost = createServerHost([file1, lib]); + const resolutionTrace = createHostModuleResolutionTrace(host); + const projectService = createProjectService(host, { typingsInstaller: new TestTypingsInstaller("/a/cache", /*throttleLimit*/5, host) }); + + projectService.setCompilerOptionsForInferredProjects({ traceResolution: true, allowJs: true }); + projectService.openClientFile(file1.path); + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + const proj = projectService.inferredProjects[0]; + + assert.deepEqual(resolutionTrace, [ + "======== Resolving module 'lib' from '/a/b/app.js'. ========", + "Module resolution kind is not specified, using 'NodeJs'.", + "Loading module 'lib' from 'node_modules' folder, target file type 'TypeScript'.", + "Directory '/a/b/node_modules' does not exist, skipping all lookups in it.", + "Directory '/a/node_modules' does not exist, skipping all lookups in it.", + "Directory '/node_modules' does not exist, skipping all lookups in it.", + "Loading module 'lib' from 'node_modules' folder, target file type 'JavaScript'.", + "Directory '/a/b/node_modules' does not exist, skipping all lookups in it.", + "Directory '/a/node_modules' does not exist, skipping all lookups in it.", + "Directory '/node_modules' does not exist, skipping all lookups in it.", + "======== Module name 'lib' was not resolved. ========", + `Auto discovery for typings is enabled in project '${proj.getProjectName()}'. Running extra resolution pass for module 'lib' using cache location '/a/cache'.`, + "File '/a/cache/node_modules/lib.d.ts' does not exist.", + "File '/a/cache/node_modules/@types/lib/package.json' does not exist.", + "File '/a/cache/node_modules/@types/lib.d.ts' does not exist.", + "File '/a/cache/node_modules/@types/lib/index.d.ts' exist - use it as a name resolution result.", + ]); + checkProjectActualFiles(proj, [file1.path, lib.path]); + }); + }); + + describe("resolutionCache:: tsserverProjectSystem module resolution caching", () => { + const projectLocation = "/user/username/projects/myproject"; + const configFile: File = { + path: `${projectLocation}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { traceResolution: true } }) + }; + + function getModules(module1Path: string, module2Path: string) { + const module1: File = { + path: module1Path, + content: `export function module1() {}` + }; + const module2: File = { + path: module2Path, + content: `export function module2() {}` + }; + return { module1, module2 }; + } + + function verifyTrace(resolutionTrace: string[], expected: string[]) { + assert.deepEqual(resolutionTrace, expected); + resolutionTrace.length = 0; + } + + function getExpectedFileDoesNotExistResolutionTrace(host: TestServerHost, expectedTrace: string[], foundModule: boolean, module: File, directory: string, file: string, ignoreIfParentMissing?: boolean) { + if (!foundModule) { + const path = combinePaths(directory, file); + if (!ignoreIfParentMissing || host.directoryExists(getDirectoryPath(path))) { + if (module.path === path) { + foundModule = true; + } + else { + expectedTrace.push(`File '${path}' does not exist.`); + } + } + } + return foundModule; + } + + function getExpectedMissedLocationResolutionTrace(host: TestServerHost, expectedTrace: string[], dirPath: string, module: File, moduleName: string, useNodeModules: boolean, cacheLocation?: string) { + let foundModule = false; + forEachAncestorDirectory(dirPath, dirPath => { + if (dirPath === cacheLocation) { + return foundModule; + } + + const directory = useNodeModules ? combinePaths(dirPath, nodeModules) : dirPath; + if (useNodeModules && !foundModule && !host.directoryExists(directory)) { + expectedTrace.push(`Directory '${directory}' does not exist, skipping all lookups in it.`); + return undefined; + } + foundModule = getExpectedFileDoesNotExistResolutionTrace(host, expectedTrace, foundModule, module, directory, `${moduleName}/package.json`, /*ignoreIfParentMissing*/ true); + foundModule = getExpectedFileDoesNotExistResolutionTrace(host, expectedTrace, foundModule, module, directory, `${moduleName}.ts`); + foundModule = getExpectedFileDoesNotExistResolutionTrace(host, expectedTrace, foundModule, module, directory, `${moduleName}.tsx`); + foundModule = getExpectedFileDoesNotExistResolutionTrace(host, expectedTrace, foundModule, module, directory, `${moduleName}.d.ts`); + foundModule = getExpectedFileDoesNotExistResolutionTrace(host, expectedTrace, foundModule, module, directory, `${moduleName}/index.ts`, /*ignoreIfParentMissing*/ true); + if (useNodeModules && !foundModule) { + expectedTrace.push(`Directory '${directory}/@types' does not exist, skipping all lookups in it.`); + } + return foundModule ? true : undefined; + }); + } + + function getExpectedResolutionTraceHeader(expectedTrace: string[], file: File, moduleName: string) { + expectedTrace.push( + `======== Resolving module '${moduleName}' from '${file.path}'. ========`, + `Module resolution kind is not specified, using 'NodeJs'.` + ); + } + + function getExpectedResolutionTraceFooter(expectedTrace: string[], module: File, moduleName: string, addRealPathTrace: boolean, ignoreModuleFileFound?: boolean) { + if (!ignoreModuleFileFound) { + expectedTrace.push(`File '${module.path}' exist - use it as a name resolution result.`); + } + if (addRealPathTrace) { + expectedTrace.push(`Resolving real path for '${module.path}', result '${module.path}'.`); + } + expectedTrace.push(`======== Module name '${moduleName}' was successfully resolved to '${module.path}'. ========`); + } + + function getExpectedRelativeModuleResolutionTrace(host: TestServerHost, file: File, module: File, moduleName: string, expectedTrace: string[] = []) { + getExpectedResolutionTraceHeader(expectedTrace, file, moduleName); + expectedTrace.push(`Loading module as file / folder, candidate module location '${removeFileExtension(module.path)}', target file type 'TypeScript'.`); + getExpectedMissedLocationResolutionTrace(host, expectedTrace, getDirectoryPath(normalizePath(combinePaths(getDirectoryPath(file.path), moduleName))), module, moduleName.substring(moduleName.lastIndexOf("/") + 1), /*useNodeModules*/ false); + getExpectedResolutionTraceFooter(expectedTrace, module, moduleName, /*addRealPathTrace*/ false); + return expectedTrace; + } + + function getExpectedNonRelativeModuleResolutionTrace(host: TestServerHost, file: File, module: File, moduleName: string, expectedTrace: string[] = []) { + getExpectedResolutionTraceHeader(expectedTrace, file, moduleName); + expectedTrace.push(`Loading module '${moduleName}' from 'node_modules' folder, target file type 'TypeScript'.`); + getExpectedMissedLocationResolutionTrace(host, expectedTrace, getDirectoryPath(file.path), module, moduleName, /*useNodeModules*/ true); + getExpectedResolutionTraceFooter(expectedTrace, module, moduleName, /*addRealPathTrace*/ true); + return expectedTrace; + } + + function getExpectedNonRelativeModuleResolutionFromCacheTrace(host: TestServerHost, file: File, module: File, moduleName: string, cacheLocation: string, expectedTrace: string[] = []) { + getExpectedResolutionTraceHeader(expectedTrace, file, moduleName); + expectedTrace.push(`Loading module '${moduleName}' from 'node_modules' folder, target file type 'TypeScript'.`); + getExpectedMissedLocationResolutionTrace(host, expectedTrace, getDirectoryPath(file.path), module, moduleName, /*useNodeModules*/ true, cacheLocation); + expectedTrace.push(`Resolution for module '${moduleName}' was found in cache from location '${cacheLocation}'.`); + getExpectedResolutionTraceFooter(expectedTrace, module, moduleName, /*addRealPathTrace*/ false, /*ignoreModuleFileFound*/ true); + return expectedTrace; + } + + function getExpectedReusingResolutionFromOldProgram(file: File, moduleName: string) { + return `Reusing resolution of module '${moduleName}' to file '${file.path}' from old program.`; + } + + function verifyWatchesWithConfigFile(host: TestServerHost, files: File[], openFile: File, extraExpectedDirectories?: ReadonlyArray) { + const expectedRecursiveDirectories = arrayToSet([projectLocation, `${projectLocation}/${nodeModulesAtTypes}`, ...(extraExpectedDirectories || emptyArray)]); + checkWatchedFiles(host, mapDefined(files, f => { + if (f === openFile) { + return undefined; + } + const indexOfNodeModules = f.path.indexOf("/node_modules/"); + if (indexOfNodeModules === -1) { + return f.path; + } + expectedRecursiveDirectories.set(f.path.substr(0, indexOfNodeModules + "/node_modules".length), true); + return undefined; + })); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, arrayFrom(expectedRecursiveDirectories.keys()), /*recursive*/ true); + } + + describe("from files in same folder", () => { + function getFiles(fileContent: string) { + const file1: File = { + path: `${projectLocation}/src/file1.ts`, + content: fileContent + }; + const file2: File = { + path: `${projectLocation}/src/file2.ts`, + content: fileContent + }; + return { file1, file2 }; + } + + it("relative module name", () => { + const module1Name = "./module1"; + const module2Name = "../module2"; + const fileContent = `import { module1 } from "${module1Name}";import { module2 } from "${module2Name}";`; + const { file1, file2 } = getFiles(fileContent); + const { module1, module2 } = getModules(`${projectLocation}/src/module1.ts`, `${projectLocation}/module2.ts`); + const files = [module1, module2, file1, file2, configFile, libFile]; + const host = createServerHost(files); + const resolutionTrace = createHostModuleResolutionTrace(host); + const service = createProjectService(host); + service.openClientFile(file1.path); + const expectedTrace = getExpectedRelativeModuleResolutionTrace(host, file1, module1, module1Name); + getExpectedRelativeModuleResolutionTrace(host, file1, module2, module2Name, expectedTrace); + verifyTrace(resolutionTrace, expectedTrace); + verifyWatchesWithConfigFile(host, files, file1); + + file1.content += fileContent; + file2.content += fileContent; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + verifyTrace(resolutionTrace, [ + getExpectedReusingResolutionFromOldProgram(file1, module1Name), + getExpectedReusingResolutionFromOldProgram(file1, module2Name) + ]); + verifyWatchesWithConfigFile(host, files, file1); + }); + + it("non relative module name", () => { + const expectedNonRelativeDirectories = [`${projectLocation}/node_modules`, `${projectLocation}/src`]; + const module1Name = "module1"; + const module2Name = "module2"; + const fileContent = `import { module1 } from "${module1Name}";import { module2 } from "${module2Name}";`; + const { file1, file2 } = getFiles(fileContent); + const { module1, module2 } = getModules(`${projectLocation}/src/node_modules/module1/index.ts`, `${projectLocation}/node_modules/module2/index.ts`); + const files = [module1, module2, file1, file2, configFile, libFile]; + const host = createServerHost(files); + const resolutionTrace = createHostModuleResolutionTrace(host); + const service = createProjectService(host); + service.openClientFile(file1.path); + const expectedTrace = getExpectedNonRelativeModuleResolutionTrace(host, file1, module1, module1Name); + getExpectedNonRelativeModuleResolutionTrace(host, file1, module2, module2Name, expectedTrace); + verifyTrace(resolutionTrace, expectedTrace); + verifyWatchesWithConfigFile(host, files, file1, expectedNonRelativeDirectories); + + file1.content += fileContent; + file2.content += fileContent; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + verifyTrace(resolutionTrace, [ + getExpectedReusingResolutionFromOldProgram(file1, module1Name), + getExpectedReusingResolutionFromOldProgram(file1, module2Name) + ]); + verifyWatchesWithConfigFile(host, files, file1, expectedNonRelativeDirectories); + }); + }); + + describe("from files in different folders", () => { + function getFiles(fileContent1: string, fileContent2 = fileContent1, fileContent3 = fileContent1, fileContent4 = fileContent1) { + const file1: File = { + path: `${projectLocation}/product/src/file1.ts`, + content: fileContent1 + }; + const file2: File = { + path: `${projectLocation}/product/src/feature/file2.ts`, + content: fileContent2 + }; + const file3: File = { + path: `${projectLocation}/product/test/src/file3.ts`, + content: fileContent3 + }; + const file4: File = { + path: `${projectLocation}/product/test/file4.ts`, + content: fileContent4 + }; + return { file1, file2, file3, file4 }; + } + + it("relative module name", () => { + const module1Name = "./module1"; + const module2Name = "../module2"; + const module3Name = "../module1"; + const module4Name = "../../module2"; + const module5Name = "../../src/module1"; + const module6Name = "../src/module1"; + const fileContent1 = `import { module1 } from "${module1Name}";import { module2 } from "${module2Name}";`; + const fileContent2 = `import { module1 } from "${module3Name}";import { module2 } from "${module4Name}";`; + const fileContent3 = `import { module1 } from "${module5Name}";import { module2 } from "${module4Name}";`; + const fileContent4 = `import { module1 } from "${module6Name}";import { module2 } from "${module2Name}";`; + const { file1, file2, file3, file4 } = getFiles(fileContent1, fileContent2, fileContent3, fileContent4); + const { module1, module2 } = getModules(`${projectLocation}/product/src/module1.ts`, `${projectLocation}/product/module2.ts`); + const files = [module1, module2, file1, file2, file3, file4, configFile, libFile]; + const host = createServerHost(files); + const resolutionTrace = createHostModuleResolutionTrace(host); + const service = createProjectService(host); + service.openClientFile(file1.path); + const expectedTrace = getExpectedRelativeModuleResolutionTrace(host, file1, module1, module1Name); + getExpectedRelativeModuleResolutionTrace(host, file1, module2, module2Name, expectedTrace); + getExpectedRelativeModuleResolutionTrace(host, file2, module1, module3Name, expectedTrace); + getExpectedRelativeModuleResolutionTrace(host, file2, module2, module4Name, expectedTrace); + getExpectedRelativeModuleResolutionTrace(host, file4, module1, module6Name, expectedTrace); + getExpectedRelativeModuleResolutionTrace(host, file4, module2, module2Name, expectedTrace); + getExpectedRelativeModuleResolutionTrace(host, file3, module1, module5Name, expectedTrace); + getExpectedRelativeModuleResolutionTrace(host, file3, module2, module4Name, expectedTrace); + verifyTrace(resolutionTrace, expectedTrace); + verifyWatchesWithConfigFile(host, files, file1); + + file1.content += fileContent1; + file2.content += fileContent2; + file3.content += fileContent3; + file4.content += fileContent4; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + + verifyTrace(resolutionTrace, [ + getExpectedReusingResolutionFromOldProgram(file1, module1Name), + getExpectedReusingResolutionFromOldProgram(file1, module2Name) + ]); + verifyWatchesWithConfigFile(host, files, file1); + }); + + it("non relative module name", () => { + const expectedNonRelativeDirectories = [`${projectLocation}/node_modules`, `${projectLocation}/product`]; + const module1Name = "module1"; + const module2Name = "module2"; + const fileContent = `import { module1 } from "${module1Name}";import { module2 } from "${module2Name}";`; + const { file1, file2, file3, file4 } = getFiles(fileContent); + const { module1, module2 } = getModules(`${projectLocation}/product/node_modules/module1/index.ts`, `${projectLocation}/node_modules/module2/index.ts`); + const files = [module1, module2, file1, file2, file3, file4, configFile, libFile]; + const host = createServerHost(files); + const resolutionTrace = createHostModuleResolutionTrace(host); + const service = createProjectService(host); + service.openClientFile(file1.path); + const expectedTrace = getExpectedNonRelativeModuleResolutionTrace(host, file1, module1, module1Name); + getExpectedNonRelativeModuleResolutionTrace(host, file1, module2, module2Name, expectedTrace); + getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file2, module1, module1Name, getDirectoryPath(file1.path), expectedTrace); + getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file2, module2, module2Name, getDirectoryPath(file1.path), expectedTrace); + getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file4, module1, module1Name, `${projectLocation}/product`, expectedTrace); + getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file4, module2, module2Name, `${projectLocation}/product`, expectedTrace); + getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file3, module1, module1Name, getDirectoryPath(file4.path), expectedTrace); + getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file3, module2, module2Name, getDirectoryPath(file4.path), expectedTrace); + verifyTrace(resolutionTrace, expectedTrace); + verifyWatchesWithConfigFile(host, files, file1, expectedNonRelativeDirectories); + + file1.content += fileContent; + file2.content += fileContent; + file3.content += fileContent; + file4.content += fileContent; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + + verifyTrace(resolutionTrace, [ + getExpectedReusingResolutionFromOldProgram(file1, module1Name), + getExpectedReusingResolutionFromOldProgram(file1, module2Name) + ]); + verifyWatchesWithConfigFile(host, files, file1, expectedNonRelativeDirectories); + }); + + it("non relative module name from inferred project", () => { + const module1Name = "module1"; + const module2Name = "module2"; + const file2Name = "./feature/file2"; + const file3Name = "../test/src/file3"; + const file4Name = "../test/file4"; + const importModuleContent = `import { module1 } from "${module1Name}";import { module2 } from "${module2Name}";`; + const { file1, file2, file3, file4 } = getFiles(`import "${file2Name}"; import "${file4Name}"; import "${file3Name}"; ${importModuleContent}`, importModuleContent, importModuleContent, importModuleContent); + const { module1, module2 } = getModules(`${projectLocation}/product/node_modules/module1/index.ts`, `${projectLocation}/node_modules/module2/index.ts`); + const files = [module1, module2, file1, file2, file3, file4, libFile]; + const host = createServerHost(files); + const resolutionTrace = createHostModuleResolutionTrace(host); + const service = createProjectService(host); + service.setCompilerOptionsForInferredProjects({ traceResolution: true }); + service.openClientFile(file1.path); + const expectedTrace = getExpectedRelativeModuleResolutionTrace(host, file1, file2, file2Name); + getExpectedRelativeModuleResolutionTrace(host, file1, file4, file4Name, expectedTrace); + getExpectedRelativeModuleResolutionTrace(host, file1, file3, file3Name, expectedTrace); + getExpectedNonRelativeModuleResolutionTrace(host, file1, module1, module1Name, expectedTrace); + getExpectedNonRelativeModuleResolutionTrace(host, file1, module2, module2Name, expectedTrace); + getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file2, module1, module1Name, getDirectoryPath(file1.path), expectedTrace); + getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file2, module2, module2Name, getDirectoryPath(file1.path), expectedTrace); + getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file4, module1, module1Name, `${projectLocation}/product`, expectedTrace); + getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file4, module2, module2Name, `${projectLocation}/product`, expectedTrace); + getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file3, module1, module1Name, getDirectoryPath(file4.path), expectedTrace); + getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file3, module2, module2Name, getDirectoryPath(file4.path), expectedTrace); + verifyTrace(resolutionTrace, expectedTrace); + + const currentDirectory = getDirectoryPath(file1.path); + const watchedFiles = mapDefined(files, f => f === file1 || f.path.indexOf("/node_modules/") !== -1 ? undefined : f.path); + forEachAncestorDirectory(currentDirectory, d => { + watchedFiles.push(combinePaths(d, "tsconfig.json"), combinePaths(d, "jsconfig.json")); + }); + const watchedRecursiveDirectories = getTypeRootsFromLocation(currentDirectory).concat([ + `${currentDirectory}/node_modules`, `${currentDirectory}/feature`, `${projectLocation}/product/${nodeModules}`, + `${projectLocation}/${nodeModules}`, `${projectLocation}/product/test/${nodeModules}`, + `${projectLocation}/product/test/src/${nodeModules}` + ]); + checkWatches(); + + file1.content += importModuleContent; + file2.content += importModuleContent; + file3.content += importModuleContent; + file4.content += importModuleContent; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + + verifyTrace(resolutionTrace, [ + getExpectedReusingResolutionFromOldProgram(file1, file2Name), + getExpectedReusingResolutionFromOldProgram(file1, file4Name), + getExpectedReusingResolutionFromOldProgram(file1, file3Name), + getExpectedReusingResolutionFromOldProgram(file1, module1Name), + getExpectedReusingResolutionFromOldProgram(file1, module2Name) + ]); + checkWatches(); + + function checkWatches() { + checkWatchedFiles(host, watchedFiles); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); + } + }); + }); + + describe("when watching directories for failed lookup locations in amd resolution", () => { + const projectRoot = "/user/username/projects/project"; + const nodeFile: File = { + path: `${projectRoot}/src/typings/node.d.ts`, + content: ` +declare module "fs" { + export interface something { + } +}` + }; + const electronFile: File = { + path: `${projectRoot}/src/typings/electron.d.ts`, + content: ` +declare module 'original-fs' { + import * as fs from 'fs'; + export = fs; +}` + }; + const srcFile: File = { + path: `${projectRoot}/src/somefolder/srcfile.ts`, + content: ` +import { x } from "somefolder/module1"; +import { x } from "somefolder/module2"; +const y = x;` + }; + const moduleFile: File = { + path: `${projectRoot}/src/somefolder/module1.ts`, + content: ` +export const x = 10;` + }; + const configFile: File = { + path: `${projectRoot}/src/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + module: "amd", + moduleResolution: "classic", + target: "es5", + outDir: "../out", + baseUrl: "./", + typeRoots: ["typings"] + } + }) + }; + + function verifyModuleResolution(useNodeFile: boolean) { + const files = [...(useNodeFile ? [nodeFile] : []), electronFile, srcFile, moduleFile, configFile, libFile]; + const host = createServerHost(files); + const service = createProjectService(host); + service.openClientFile(srcFile.path, srcFile.content, ScriptKind.TS, projectRoot); + checkProjectActualFiles(service.configuredProjects.get(configFile.path)!, files.map(f => f.path)); + checkWatchedFilesDetailed(host, mapDefined(files, f => f === srcFile ? undefined : f.path), 1); + if (useNodeFile) { + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); // since fs resolves to ambient module, shouldnt watch failed lookup + } + else { + checkWatchedDirectoriesDetailed(host, [`${projectRoot}`, `${projectRoot}/src`], 1, /*recursive*/ false); // failed lookup for fs + } + const expectedWatchedDirectories = createMap(); + expectedWatchedDirectories.set(`${projectRoot}/src`, 1); // Wild card + expectedWatchedDirectories.set(`${projectRoot}/src/somefolder`, 1); // failedLookup for somefolder/module2 + expectedWatchedDirectories.set(`${projectRoot}/src/node_modules`, 1); // failed lookup for somefolder/module2 + expectedWatchedDirectories.set(`${projectRoot}/somefolder`, 1); // failed lookup for somefolder/module2 + expectedWatchedDirectories.set(`${projectRoot}/node_modules`, 1); // failed lookup for with node_modules/@types/fs + expectedWatchedDirectories.set(`${projectRoot}/src/typings`, useNodeFile ? 1 : 2); // typeroot directory + failed lookup if not using node file + checkWatchedDirectoriesDetailed(host, expectedWatchedDirectories, /*recursive*/ true); + } + + it("when resolves to ambient module", () => { + verifyModuleResolution(/*useNodeFile*/ true); + }); + + it("when resolution fails", () => { + verifyModuleResolution(/*useNodeFile*/ false); + }); + }); + + describe("ignores files/folder changes in node_modules that start with '.'", () => { + const projectPath = "/user/username/projects/project"; + const npmCacheFile: File = { + path: `${projectPath}/node_modules/.cache/babel-loader/89c02171edab901b9926470ba6d5677e.ts`, + content: JSON.stringify({ something: 10 }) + }; + const file1: File = { + path: `${projectPath}/test.ts`, + content: `import { x } from "somemodule";` + }; + const file2: File = { + path: `${projectPath}/node_modules/somemodule/index.d.ts`, + content: `export const x = 10;` + }; + it("when watching node_modules in inferred project for failed lookup/closed script infos", () => { + const files = [libFile, file1, file2]; + const host = createServerHost(files); + const service = createProjectService(host); + service.openClientFile(file1.path); + checkNumberOfProjects(service, { inferredProjects: 1 }); + const project = service.inferredProjects[0]; + checkProjectActualFiles(project, files.map(f => f.path)); + (project as ResolutionCacheHost).maxNumberOfFilesToIterateForInvalidation = 1; + host.checkTimeoutQueueLength(0); + + host.ensureFileOrFolder(npmCacheFile); + host.checkTimeoutQueueLength(0); + }); + it("when watching node_modules as part of wild card directories in config project", () => { + const config: File = { + path: `${projectPath}/tsconfig.json`, + content: "{}" + }; + const files = [libFile, file1, file2, config]; + const host = createServerHost(files); + const service = createProjectService(host); + service.openClientFile(file1.path); + checkNumberOfProjects(service, { configuredProjects: 1 }); + const project = Debug.assertDefined(service.configuredProjects.get(config.path)); + checkProjectActualFiles(project, files.map(f => f.path)); + host.checkTimeoutQueueLength(0); + + host.ensureFileOrFolder(npmCacheFile); + host.checkTimeoutQueueLength(0); + }); + }); + }); +} diff --git a/src/testRunner/unittests/tsbuildWatchMode.ts b/src/testRunner/unittests/tsbuildWatchMode.ts index 9178416373..9f45da5165 100644 --- a/src/testRunner/unittests/tsbuildWatchMode.ts +++ b/src/testRunner/unittests/tsbuildWatchMode.ts @@ -1,5 +1,4 @@ namespace ts.tscWatch { - export import libFile = TestFSWithWatch.libFile; import projectsLocation = TestFSWithWatch.tsbuildProjectsLocation; import getFilePathInProject = TestFSWithWatch.getTsBuildProjectFilePath; import getFileFromProject = TestFSWithWatch.getTsBuildProjectFile; diff --git a/src/testRunner/unittests/tscWatchHelpers.ts b/src/testRunner/unittests/tscWatchHelpers.ts new file mode 100644 index 0000000000..6e3fd40c49 --- /dev/null +++ b/src/testRunner/unittests/tscWatchHelpers.ts @@ -0,0 +1,237 @@ +namespace ts.tscWatch { + export import WatchedSystem = TestFSWithWatch.TestServerHost; + export type File = TestFSWithWatch.File; + export type SymLink = TestFSWithWatch.SymLink; + export import libFile = TestFSWithWatch.libFile; + export import createWatchedSystem = TestFSWithWatch.createWatchedSystem; + export import checkArray = TestFSWithWatch.checkArray; + export import checkWatchedFiles = TestFSWithWatch.checkWatchedFiles; + export import checkWatchedFilesDetailed = TestFSWithWatch.checkWatchedFilesDetailed; + export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories; + export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed; + export import checkOutputContains = TestFSWithWatch.checkOutputContains; + export import checkOutputDoesNotContain = TestFSWithWatch.checkOutputDoesNotContain; + + export function checkProgramActualFiles(program: Program, expectedFiles: ReadonlyArray) { + checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); + } + + export function checkProgramRootFiles(program: Program, expectedFiles: ReadonlyArray) { + checkArray(`Program rootFileNames`, program.getRootFileNames(), expectedFiles); + } + + export function createWatchOfConfigFileReturningBuilder(configFileName: string, host: WatchedSystem, maxNumberOfFilesToIterateForInvalidation?: number) { + const compilerHost = createWatchCompilerHostOfConfigFile(configFileName, {}, host); + compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; + const watch = createWatchProgram(compilerHost); + return () => watch.getCurrentProgram(); + } + + export function createWatchOfConfigFile(configFileName: string, host: WatchedSystem, maxNumberOfFilesToIterateForInvalidation?: number) { + const compilerHost = createWatchCompilerHostOfConfigFile(configFileName, {}, host); + compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; + const watch = createWatchProgram(compilerHost); + return () => watch.getCurrentProgram().getProgram(); + } + + export function createWatchOfFilesAndCompilerOptions(rootFiles: string[], host: WatchedSystem, options: CompilerOptions = {}, maxNumberOfFilesToIterateForInvalidation?: number) { + const compilerHost = createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, host); + compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; + const watch = createWatchProgram(compilerHost); + return () => watch.getCurrentProgram().getProgram(); + } + + //function getEmittedLineForMultiFileOutput(file: File, host: WatchedSystem) { + // return `TSFILE: ${file.path.replace(".ts", ".js")}${host.newLine}`; + //} + + //function getEmittedLineForSingleFileOutput(filename: string, host: WatchedSystem) { + // return `TSFILE: ${filename}${host.newLine}`; + //} + + //interface FileOrFolderEmit extends File { + // output?: string; + //} + + //function getFileOrFolderEmit(file: File, getOutput?: (file: File) => string): FileOrFolderEmit { + // const result = file as FileOrFolderEmit; + // if (getOutput) { + // result.output = getOutput(file); + // } + // return result; + //} + + //function getEmittedLines(files: FileOrFolderEmit[]) { + // const seen = createMap(); + // const result: string[] = []; + // for (const { output } of files) { + // if (output && !seen.has(output)) { + // seen.set(output, true); + // result.push(output); + // } + // } + // return result; + //} + + //function checkAffectedLines(host: WatchedSystem, affectedFiles: FileOrFolderEmit[], allEmittedFiles: string[]) { + // const expectedAffectedFiles = getEmittedLines(affectedFiles); + // const expectedNonAffectedFiles = mapDefined(allEmittedFiles, line => contains(expectedAffectedFiles, line) ? undefined : line); + // checkOutputContains(host, expectedAffectedFiles); + // checkOutputDoesNotContain(host, expectedNonAffectedFiles); + //} + + const elapsedRegex = /^Elapsed:: [0-9]+ms/; + function checkOutputErrors( + host: WatchedSystem, + logsBeforeWatchDiagnostic: string[] | undefined, + preErrorsWatchDiagnostic: Diagnostic, + logsBeforeErrors: string[] | undefined, + errors: ReadonlyArray | ReadonlyArray, + disableConsoleClears?: boolean | undefined, + ...postErrorsWatchDiagnostics: Diagnostic[] + ) { + let screenClears = 0; + const outputs = host.getOutput(); + const expectedOutputCount = 1 + errors.length + postErrorsWatchDiagnostics.length + + (logsBeforeWatchDiagnostic ? logsBeforeWatchDiagnostic.length : 0) + (logsBeforeErrors ? logsBeforeErrors.length : 0); + assert.equal(outputs.length, expectedOutputCount, JSON.stringify(outputs)); + let index = 0; + forEach(logsBeforeWatchDiagnostic, log => assertLog("logsBeforeWatchDiagnostic", log)); + assertWatchDiagnostic(preErrorsWatchDiagnostic); + forEach(logsBeforeErrors, log => assertLog("logBeforeError", log)); + // Verify errors + forEach(errors, assertDiagnostic); + forEach(postErrorsWatchDiagnostics, assertWatchDiagnostic); + assert.equal(host.screenClears.length, screenClears, "Expected number of screen clears"); + host.clearOutput(); + + function isDiagnostic(diagnostic: Diagnostic | string): diagnostic is Diagnostic { + return !!(diagnostic as Diagnostic).messageText; + } + + function assertDiagnostic(diagnostic: Diagnostic | string) { + const expected = isDiagnostic(diagnostic) ? formatDiagnostic(diagnostic, host) : diagnostic; + assert.equal(outputs[index], expected, getOutputAtFailedMessage("Diagnostic", expected)); + index++; + } + + function assertLog(caption: string, expected: string) { + const actual = outputs[index]; + assert.equal(actual.replace(elapsedRegex, ""), expected.replace(elapsedRegex, ""), getOutputAtFailedMessage(caption, expected)); + index++; + } + + function assertWatchDiagnostic(diagnostic: Diagnostic) { + const expected = getWatchDiagnosticWithoutDate(diagnostic); + if (!disableConsoleClears && contains(screenStartingMessageCodes, diagnostic.code)) { + assert.equal(host.screenClears[screenClears], index, `Expected screen clear at this diagnostic: ${expected}`); + screenClears++; + } + assert.isTrue(endsWith(outputs[index], expected), getOutputAtFailedMessage("Watch diagnostic", expected)); + index++; + } + + function getOutputAtFailedMessage(caption: string, expectedOutput: string) { + return `Expected ${caption}: ${JSON.stringify(expectedOutput)} at ${index} in ${JSON.stringify(outputs)}`; + } + + function getWatchDiagnosticWithoutDate(diagnostic: Diagnostic) { + const newLines = contains(screenStartingMessageCodes, diagnostic.code) + ? `${host.newLine}${host.newLine}` + : host.newLine; + return ` - ${flattenDiagnosticMessageText(diagnostic.messageText, host.newLine)}${newLines}`; + } + } + + function createErrorsFoundCompilerDiagnostic(errors: ReadonlyArray | ReadonlyArray) { + return errors.length === 1 + ? createCompilerDiagnostic(Diagnostics.Found_1_error_Watching_for_file_changes) + : createCompilerDiagnostic(Diagnostics.Found_0_errors_Watching_for_file_changes, errors.length); + } + + export function checkOutputErrorsInitial(host: WatchedSystem, errors: ReadonlyArray | ReadonlyArray, disableConsoleClears?: boolean, logsBeforeErrors?: string[]) { + checkOutputErrors( + host, + /*logsBeforeWatchDiagnostic*/ undefined, + createCompilerDiagnostic(Diagnostics.Starting_compilation_in_watch_mode), + logsBeforeErrors, + errors, + disableConsoleClears, + createErrorsFoundCompilerDiagnostic(errors)); + } + + export function checkOutputErrorsIncremental(host: WatchedSystem, errors: ReadonlyArray | ReadonlyArray, disableConsoleClears?: boolean, logsBeforeWatchDiagnostic?: string[], logsBeforeErrors?: string[]) { + checkOutputErrors( + host, + logsBeforeWatchDiagnostic, + createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation), + logsBeforeErrors, + errors, + disableConsoleClears, + createErrorsFoundCompilerDiagnostic(errors)); + } + + export function checkOutputErrorsIncrementalWithExit(host: WatchedSystem, errors: ReadonlyArray | ReadonlyArray, expectedExitCode: ExitStatus, disableConsoleClears?: boolean, logsBeforeWatchDiagnostic?: string[], logsBeforeErrors?: string[]) { + checkOutputErrors( + host, + logsBeforeWatchDiagnostic, + createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation), + logsBeforeErrors, + errors, + disableConsoleClears); + assert.equal(host.exitCode, expectedExitCode); + } + + function getDiagnosticOfFileFrom(file: SourceFile | undefined, text: string, start: number | undefined, length: number | undefined, message: DiagnosticMessage): Diagnostic { + return { + file, + start, + length, + + messageText: text, + category: message.category, + code: message.code, + }; + } + + //function getDiagnosticWithoutFile(message: DiagnosticMessage, ..._args: (string | number)[]): Diagnostic { + // let text = getLocaleSpecificMessage(message); + + // if (arguments.length > 1) { + // text = formatStringFromArgs(text, arguments, 1); + // } + + // return getDiagnosticOfFileFrom(/*file*/ undefined, text, /*start*/ undefined, /*length*/ undefined, message); + //} + + //function getDiagnosticOfFile(file: SourceFile, start: number, length: number, message: DiagnosticMessage, ..._args: (string | number)[]): Diagnostic { + // let text = getLocaleSpecificMessage(message); + + // if (arguments.length > 4) { + // text = formatStringFromArgs(text, arguments, 4); + // } + + // return getDiagnosticOfFileFrom(file, text, start, length, message); + //} + + //function getUnknownCompilerOption(program: Program, configFile: File, option: string) { + // const quotedOption = `"${option}"`; + // return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0, option); + //} + + export function getDiagnosticOfFileFromProgram(program: Program, filePath: string, start: number, length: number, message: DiagnosticMessage, ..._args: (string | number)[]): Diagnostic { + let text = getLocaleSpecificMessage(message); + + if (arguments.length > 5) { + text = formatStringFromArgs(text, arguments, 5); + } + + return getDiagnosticOfFileFrom(program.getSourceFileByPath(toPath(filePath, program.getCurrentDirectory(), s => s.toLowerCase()))!, + text, start, length, message); + } + + export function getDiagnosticModuleNotFoundOfFile(program: Program, file: File, moduleName: string) { + const quotedModuleName = `"${moduleName}"`; + return getDiagnosticOfFileFromProgram(program, file.path, file.content.indexOf(quotedModuleName), quotedModuleName.length, Diagnostics.Cannot_find_module_0, moduleName); + } +} diff --git a/src/testRunner/unittests/tscWatchMode.ts b/src/testRunner/unittests/tscWatchMode.ts index 03bead8178..662f26e8b1 100644 --- a/src/testRunner/unittests/tscWatchMode.ts +++ b/src/testRunner/unittests/tscWatchMode.ts @@ -1,45 +1,4 @@ namespace ts.tscWatch { - export import WatchedSystem = TestFSWithWatch.TestServerHost; - export type File = TestFSWithWatch.File; - export type SymLink = TestFSWithWatch.SymLink; - export import createWatchedSystem = TestFSWithWatch.createWatchedSystem; - export import checkArray = TestFSWithWatch.checkArray; - export import checkWatchedFiles = TestFSWithWatch.checkWatchedFiles; - export import checkWatchedFilesDetailed = TestFSWithWatch.checkWatchedFilesDetailed; - export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories; - export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed; - export import checkOutputContains = TestFSWithWatch.checkOutputContains; - export import checkOutputDoesNotContain = TestFSWithWatch.checkOutputDoesNotContain; - - export function checkProgramActualFiles(program: Program, expectedFiles: ReadonlyArray) { - checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); - } - - export function checkProgramRootFiles(program: Program, expectedFiles: ReadonlyArray) { - checkArray(`Program rootFileNames`, program.getRootFileNames(), expectedFiles); - } - - export function createWatchOfConfigFileReturningBuilder(configFileName: string, host: WatchedSystem, maxNumberOfFilesToIterateForInvalidation?: number) { - const compilerHost = createWatchCompilerHostOfConfigFile(configFileName, {}, host); - compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; - const watch = createWatchProgram(compilerHost); - return () => watch.getCurrentProgram(); - } - - export function createWatchOfConfigFile(configFileName: string, host: WatchedSystem, maxNumberOfFilesToIterateForInvalidation?: number) { - const compilerHost = createWatchCompilerHostOfConfigFile(configFileName, {}, host); - compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; - const watch = createWatchProgram(compilerHost); - return () => watch.getCurrentProgram().getProgram(); - } - - export function createWatchOfFilesAndCompilerOptions(rootFiles: string[], host: WatchedSystem, options: CompilerOptions = {}, maxNumberOfFilesToIterateForInvalidation?: number) { - const compilerHost = createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, host); - compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; - const watch = createWatchProgram(compilerHost); - return () => watch.getCurrentProgram().getProgram(); - } - function getEmittedLineForMultiFileOutput(file: File, host: WatchedSystem) { return `TSFILE: ${file.path.replace(".ts", ".js")}${host.newLine}`; } @@ -79,108 +38,6 @@ namespace ts.tscWatch { checkOutputDoesNotContain(host, expectedNonAffectedFiles); } - const elapsedRegex = /^Elapsed:: [0-9]+ms/; - function checkOutputErrors( - host: WatchedSystem, - logsBeforeWatchDiagnostic: string[] | undefined, - preErrorsWatchDiagnostic: Diagnostic, - logsBeforeErrors: string[] | undefined, - errors: ReadonlyArray | ReadonlyArray, - disableConsoleClears?: boolean | undefined, - ...postErrorsWatchDiagnostics: Diagnostic[] - ) { - let screenClears = 0; - const outputs = host.getOutput(); - const expectedOutputCount = 1 + errors.length + postErrorsWatchDiagnostics.length + - (logsBeforeWatchDiagnostic ? logsBeforeWatchDiagnostic.length : 0) + (logsBeforeErrors ? logsBeforeErrors.length : 0); - assert.equal(outputs.length, expectedOutputCount, JSON.stringify(outputs)); - let index = 0; - forEach(logsBeforeWatchDiagnostic, log => assertLog("logsBeforeWatchDiagnostic", log)); - assertWatchDiagnostic(preErrorsWatchDiagnostic); - forEach(logsBeforeErrors, log => assertLog("logBeforeError", log)); - // Verify errors - forEach(errors, assertDiagnostic); - forEach(postErrorsWatchDiagnostics, assertWatchDiagnostic); - assert.equal(host.screenClears.length, screenClears, "Expected number of screen clears"); - host.clearOutput(); - - function isDiagnostic(diagnostic: Diagnostic | string): diagnostic is Diagnostic { - return !!(diagnostic as Diagnostic).messageText; - } - - function assertDiagnostic(diagnostic: Diagnostic | string) { - const expected = isDiagnostic(diagnostic) ? formatDiagnostic(diagnostic, host) : diagnostic; - assert.equal(outputs[index], expected, getOutputAtFailedMessage("Diagnostic", expected)); - index++; - } - - function assertLog(caption: string, expected: string) { - const actual = outputs[index]; - assert.equal(actual.replace(elapsedRegex, ""), expected.replace(elapsedRegex, ""), getOutputAtFailedMessage(caption, expected)); - index++; - } - - function assertWatchDiagnostic(diagnostic: Diagnostic) { - const expected = getWatchDiagnosticWithoutDate(diagnostic); - if (!disableConsoleClears && contains(screenStartingMessageCodes, diagnostic.code)) { - assert.equal(host.screenClears[screenClears], index, `Expected screen clear at this diagnostic: ${expected}`); - screenClears++; - } - assert.isTrue(endsWith(outputs[index], expected), getOutputAtFailedMessage("Watch diagnostic", expected)); - index++; - } - - function getOutputAtFailedMessage(caption: string, expectedOutput: string) { - return `Expected ${caption}: ${JSON.stringify(expectedOutput)} at ${index} in ${JSON.stringify(outputs)}`; - } - - function getWatchDiagnosticWithoutDate(diagnostic: Diagnostic) { - const newLines = contains(screenStartingMessageCodes, diagnostic.code) - ? `${host.newLine}${host.newLine}` - : host.newLine; - return ` - ${flattenDiagnosticMessageText(diagnostic.messageText, host.newLine)}${newLines}`; - } - } - - function createErrorsFoundCompilerDiagnostic(errors: ReadonlyArray | ReadonlyArray) { - return errors.length === 1 - ? createCompilerDiagnostic(Diagnostics.Found_1_error_Watching_for_file_changes) - : createCompilerDiagnostic(Diagnostics.Found_0_errors_Watching_for_file_changes, errors.length); - } - - export function checkOutputErrorsInitial(host: WatchedSystem, errors: ReadonlyArray | ReadonlyArray, disableConsoleClears?: boolean, logsBeforeErrors?: string[]) { - checkOutputErrors( - host, - /*logsBeforeWatchDiagnostic*/ undefined, - createCompilerDiagnostic(Diagnostics.Starting_compilation_in_watch_mode), - logsBeforeErrors, - errors, - disableConsoleClears, - createErrorsFoundCompilerDiagnostic(errors)); - } - - export function checkOutputErrorsIncremental(host: WatchedSystem, errors: ReadonlyArray | ReadonlyArray, disableConsoleClears?: boolean, logsBeforeWatchDiagnostic?: string[], logsBeforeErrors?: string[]) { - checkOutputErrors( - host, - logsBeforeWatchDiagnostic, - createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation), - logsBeforeErrors, - errors, - disableConsoleClears, - createErrorsFoundCompilerDiagnostic(errors)); - } - - function checkOutputErrorsIncrementalWithExit(host: WatchedSystem, errors: ReadonlyArray | ReadonlyArray, expectedExitCode: ExitStatus, disableConsoleClears?: boolean, logsBeforeWatchDiagnostic?: string[], logsBeforeErrors?: string[]) { - checkOutputErrors( - host, - logsBeforeWatchDiagnostic, - createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation), - logsBeforeErrors, - errors, - disableConsoleClears); - assert.equal(host.exitCode, expectedExitCode); - } - function getDiagnosticOfFileFrom(file: SourceFile | undefined, text: string, start: number | undefined, length: number | undefined, message: DiagnosticMessage): Diagnostic { return { file, @@ -218,22 +75,6 @@ namespace ts.tscWatch { return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0, option); } - function getDiagnosticOfFileFromProgram(program: Program, filePath: string, start: number, length: number, message: DiagnosticMessage, ..._args: (string | number)[]): Diagnostic { - let text = getLocaleSpecificMessage(message); - - if (arguments.length > 5) { - text = formatStringFromArgs(text, arguments, 5); - } - - return getDiagnosticOfFileFrom(program.getSourceFileByPath(toPath(filePath, program.getCurrentDirectory(), s => s.toLowerCase()))!, - text, start, length, message); - } - - function getDiagnosticModuleNotFoundOfFile(program: Program, file: File, moduleName: string) { - const quotedModuleName = `"${moduleName}"`; - return getDiagnosticOfFileFromProgram(program, file.path, file.content.indexOf(quotedModuleName), quotedModuleName.length, Diagnostics.Cannot_find_module_0, moduleName); - } - describe("tsc-watch program updates", () => { const commonFile1: File = { path: "/a/b/commonFile1.ts", @@ -2343,411 +2184,6 @@ interface Document { }); }); - describe("tsc-watch module resolution caching", () => { - it("works", () => { - const root = { - path: "/a/d/f0.ts", - content: `import {x} from "f1"` - }; - const imported = { - path: "/a/f1.ts", - content: `foo()` - }; - - const files = [root, imported, libFile]; - const host = createWatchedSystem(files); - const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); - - const f1IsNotModule = getDiagnosticOfFileFromProgram(watch(), root.path, root.content.indexOf('"f1"'), '"f1"'.length, Diagnostics.File_0_is_not_a_module, imported.path); - const cannotFindFoo = getDiagnosticOfFileFromProgram(watch(), imported.path, imported.content.indexOf("foo"), "foo".length, Diagnostics.Cannot_find_name_0, "foo"); - - // ensure that imported file was found - checkOutputErrorsInitial(host, [f1IsNotModule, cannotFindFoo]); - - const originalFileExists = host.fileExists; - { - const newContent = `import {x} from "f1" - var x: string = 1;`; - root.content = newContent; - host.reloadFS(files); - - // patch fileExists to make sure that disk is not touched - host.fileExists = notImplemented; - - // trigger synchronization to make sure that import will be fetched from the cache - host.runQueuedTimeoutCallbacks(); - - // ensure file has correct number of errors after edit - checkOutputErrorsIncremental(host, [ - f1IsNotModule, - getDiagnosticOfFileFromProgram(watch(), root.path, newContent.indexOf("var x") + "var ".length, "x".length, Diagnostics.Type_0_is_not_assignable_to_type_1, 1, "string"), - cannotFindFoo - ]); - } - { - let fileExistsIsCalled = false; - host.fileExists = (fileName): boolean => { - if (fileName === "lib.d.ts") { - return false; - } - fileExistsIsCalled = true; - assert.isTrue(fileName.indexOf("/f2.") !== -1); - return originalFileExists.call(host, fileName); - }; - - root.content = `import {x} from "f2"`; - host.reloadFS(files); - - // trigger synchronization to make sure that LSHost will try to find 'f2' module on disk - host.runQueuedTimeoutCallbacks(); - - // ensure file has correct number of errors after edit - checkOutputErrorsIncremental(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "f2") - ]); - - assert.isTrue(fileExistsIsCalled); - } - { - let fileExistsCalled = false; - host.fileExists = (fileName): boolean => { - if (fileName === "lib.d.ts") { - return false; - } - fileExistsCalled = true; - assert.isTrue(fileName.indexOf("/f1.") !== -1); - return originalFileExists.call(host, fileName); - }; - - const newContent = `import {x} from "f1"`; - root.content = newContent; - - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - - checkOutputErrorsIncremental(host, [f1IsNotModule, cannotFindFoo]); - assert.isTrue(fileExistsCalled); - } - }); - - it("loads missing files from disk", () => { - const root = { - path: `/a/foo.ts`, - content: `import {x} from "bar"` - }; - - const imported = { - path: `/a/bar.d.ts`, - content: `export const y = 1;` - }; - - const files = [root, libFile]; - const host = createWatchedSystem(files); - const originalFileExists = host.fileExists; - - let fileExistsCalledForBar = false; - host.fileExists = fileName => { - if (fileName === "lib.d.ts") { - return false; - } - if (!fileExistsCalledForBar) { - fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; - } - - return originalFileExists.call(host, fileName); - }; - - const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); - - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); - checkOutputErrorsInitial(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") - ]); - - fileExistsCalledForBar = false; - root.content = `import {y} from "bar"`; - host.reloadFS(files.concat(imported)); - - host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, emptyArray); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); - }); - - it("should compile correctly when resolved module goes missing and then comes back (module is not part of the root)", () => { - const root = { - path: `/a/foo.ts`, - content: `import {x} from "bar"` - }; - - const imported = { - path: `/a/bar.d.ts`, - content: `export const y = 1;export const x = 10;` - }; - - const files = [root, libFile]; - const filesWithImported = files.concat(imported); - const host = createWatchedSystem(filesWithImported); - const originalFileExists = host.fileExists; - let fileExistsCalledForBar = false; - host.fileExists = fileName => { - if (fileName === "lib.d.ts") { - return false; - } - if (!fileExistsCalledForBar) { - fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; - } - return originalFileExists.call(host, fileName); - }; - - const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); - - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); - checkOutputErrorsInitial(host, emptyArray); - - fileExistsCalledForBar = false; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); - checkOutputErrorsIncremental(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") - ]); - - fileExistsCalledForBar = false; - host.reloadFS(filesWithImported); - host.checkTimeoutQueueLengthAndRun(1); - checkOutputErrorsIncremental(host, emptyArray); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); - }); - - it("works when module resolution changes to ambient module", () => { - const root = { - path: "/a/b/foo.ts", - content: `import * as fs from "fs";` - }; - - const packageJson = { - path: "/a/b/node_modules/@types/node/package.json", - content: ` -{ - "main": "" -} -` - }; - - const nodeType = { - path: "/a/b/node_modules/@types/node/index.d.ts", - content: ` -declare module "fs" { - export interface Stats { - isFile(): boolean; - } -}` - }; - - const files = [root, libFile]; - const filesWithNodeType = files.concat(packageJson, nodeType); - const host = createWatchedSystem(files, { currentDirectory: "/a/b" }); - - const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { }); - - checkOutputErrorsInitial(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") - ]); - - host.reloadFS(filesWithNodeType); - host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, emptyArray); - }); - - it("works when included file with ambient module changes", () => { - const root = { - path: "/a/b/foo.ts", - content: ` -import * as fs from "fs"; -import * as u from "url"; -` - }; - - const file = { - path: "/a/b/bar.d.ts", - content: ` -declare module "url" { - export interface Url { - href?: string; - } -} -` - }; - - const fileContentWithFS = ` -declare module "fs" { - export interface Stats { - isFile(): boolean; - } -} -`; - - const files = [root, file, libFile]; - const host = createWatchedSystem(files, { currentDirectory: "/a/b" }); - - const watch = createWatchOfFilesAndCompilerOptions([root.path, file.path], host, {}); - - checkOutputErrorsInitial(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") - ]); - - file.content += fileContentWithFS; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, emptyArray); - }); - - it("works when reusing program with files from external library", () => { - interface ExpectedFile { path: string; isExpectedToEmit?: boolean; content?: string; } - const configDir = "/a/b/projects/myProject/src/"; - const file1: File = { - path: configDir + "file1.ts", - content: 'import module1 = require("module1");\nmodule1("hello");' - }; - const file2: File = { - path: configDir + "file2.ts", - content: 'import module11 = require("module1");\nmodule11("hello");' - }; - const module1: File = { - path: "/a/b/projects/myProject/node_modules/module1/index.js", - content: "module.exports = options => { return options.toString(); }" - }; - const configFile: File = { - path: configDir + "tsconfig.json", - content: JSON.stringify({ - compilerOptions: { - allowJs: true, - rootDir: ".", - outDir: "../dist", - moduleResolution: "node", - maxNodeModuleJsDepth: 1 - } - }) - }; - const outDirFolder = "/a/b/projects/myProject/dist/"; - const programFiles = [file1, file2, module1, libFile]; - const host = createWatchedSystem(programFiles.concat(configFile), { currentDirectory: "/a/b/projects/myProject/" }); - const watch = createWatchOfConfigFile(configFile.path, host); - checkProgramActualFiles(watch(), programFiles.map(f => f.path)); - checkOutputErrorsInitial(host, emptyArray); - const expectedFiles: ExpectedFile[] = [ - createExpectedEmittedFile(file1), - createExpectedEmittedFile(file2), - createExpectedToNotEmitFile("index.js"), - createExpectedToNotEmitFile("src/index.js"), - createExpectedToNotEmitFile("src/file1.js"), - createExpectedToNotEmitFile("src/file2.js"), - createExpectedToNotEmitFile("lib.js"), - createExpectedToNotEmitFile("lib.d.ts") - ]; - verifyExpectedFiles(expectedFiles); - - file1.content += "\n;"; - expectedFiles[0].content += ";\n"; // Only emit file1 with this change - expectedFiles[1].isExpectedToEmit = false; - host.reloadFS(programFiles.concat(configFile)); - host.runQueuedTimeoutCallbacks(); - checkProgramActualFiles(watch(), programFiles.map(f => f.path)); - checkOutputErrorsIncremental(host, emptyArray); - verifyExpectedFiles(expectedFiles); - - - function verifyExpectedFiles(expectedFiles: ExpectedFile[]) { - forEach(expectedFiles, f => { - assert.equal(!!host.fileExists(f.path), f.isExpectedToEmit, "File " + f.path + " is expected to " + (f.isExpectedToEmit ? "emit" : "not emit")); - if (f.isExpectedToEmit) { - assert.equal(host.readFile(f.path), f.content, "Expected contents of " + f.path); - } - }); - } - - function createExpectedToNotEmitFile(fileName: string): ExpectedFile { - return { - path: outDirFolder + fileName, - isExpectedToEmit: false - }; - } - - function createExpectedEmittedFile(file: File): ExpectedFile { - return { - path: removeFileExtension(file.path.replace(configDir, outDirFolder)) + Extension.Js, - isExpectedToEmit: true, - content: '"use strict";\nexports.__esModule = true;\n' + file.content.replace("import", "var") + "\n" - }; - } - }); - - it("works when renaming node_modules folder that already contains @types folder", () => { - const currentDirectory = "/user/username/projects/myproject"; - const file: File = { - path: `${currentDirectory}/a.ts`, - content: `import * as q from "qqq";` - }; - const module: File = { - path: `${currentDirectory}/node_modules2/@types/qqq/index.d.ts`, - content: "export {}" - }; - const files = [file, module, libFile]; - const host = createWatchedSystem(files, { currentDirectory }); - const watch = createWatchOfFilesAndCompilerOptions([file.path], host); - - checkProgramActualFiles(watch(), [file.path, libFile.path]); - checkOutputErrorsInitial(host, [getDiagnosticModuleNotFoundOfFile(watch(), file, "qqq")]); - checkWatchedDirectories(host, emptyArray, /*recursive*/ false); - checkWatchedDirectories(host, [`${currentDirectory}/node_modules`, `${currentDirectory}/node_modules/@types`], /*recursive*/ true); - - host.renameFolder(`${currentDirectory}/node_modules2`, `${currentDirectory}/node_modules`); - host.runQueuedTimeoutCallbacks(); - checkProgramActualFiles(watch(), [file.path, libFile.path, `${currentDirectory}/node_modules/@types/qqq/index.d.ts`]); - checkOutputErrorsIncremental(host, emptyArray); - }); - - describe("ignores files/folder changes in node_modules that start with '.'", () => { - const projectPath = "/user/username/projects/project"; - const npmCacheFile: File = { - path: `${projectPath}/node_modules/.cache/babel-loader/89c02171edab901b9926470ba6d5677e.ts`, - content: JSON.stringify({ something: 10 }) - }; - const file1: File = { - path: `${projectPath}/test.ts`, - content: `import { x } from "somemodule";` - }; - const file2: File = { - path: `${projectPath}/node_modules/somemodule/index.d.ts`, - content: `export const x = 10;` - }; - const files = [libFile, file1, file2]; - const expectedFiles = files.map(f => f.path); - it("when watching node_modules in inferred project for failed lookup", () => { - const host = createWatchedSystem(files); - const watch = createWatchOfFilesAndCompilerOptions([file1.path], host, {}, /*maxNumberOfFilesToIterateForInvalidation*/ 1); - checkProgramActualFiles(watch(), expectedFiles); - host.checkTimeoutQueueLength(0); - - host.ensureFileOrFolder(npmCacheFile); - host.checkTimeoutQueueLength(0); - }); - it("when watching node_modules as part of wild card directories in config project", () => { - const config: File = { - path: `${projectPath}/tsconfig.json`, - content: "{}" - }; - const host = createWatchedSystem(files.concat(config)); - const watch = createWatchOfConfigFile(config.path, host); - checkProgramActualFiles(watch(), expectedFiles); - host.checkTimeoutQueueLength(0); - - host.ensureFileOrFolder(npmCacheFile); - host.checkTimeoutQueueLength(0); - }); - }); - }); - describe("tsc-watch with when module emit is specified as node", () => { it("when instead of filechanged recursive directory watcher is invoked", () => { const configFile: File = { @@ -2883,85 +2319,4 @@ declare module "fs" { }); }); }); - - describe("tsc-watch with modules linked to sibling folder", () => { - const projectRoot = "/user/username/projects/project"; - const mainPackageRoot = `${projectRoot}/main`; - const linkedPackageRoot = `${projectRoot}/linked-package`; - const mainFile: File = { - path: `${mainPackageRoot}/index.ts`, - content: "import { Foo } from '@scoped/linked-package'" - }; - const config: File = { - path: `${mainPackageRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { module: "commonjs", moduleResolution: "node", baseUrl: ".", rootDir: "." }, - files: ["index.ts"] - }) - }; - const linkedPackageInMain: SymLink = { - path: `${mainPackageRoot}/node_modules/@scoped/linked-package`, - symLink: `${linkedPackageRoot}` - }; - const linkedPackageJson: File = { - path: `${linkedPackageRoot}/package.json`, - content: JSON.stringify({ name: "@scoped/linked-package", version: "0.0.1", types: "dist/index.d.ts", main: "dist/index.js" }) - }; - const linkedPackageIndex: File = { - path: `${linkedPackageRoot}/dist/index.d.ts`, - content: "export * from './other';" - }; - const linkedPackageOther: File = { - path: `${linkedPackageRoot}/dist/other.d.ts`, - content: 'export declare const Foo = "BAR";' - }; - - it("verify watched directories", () => { - const files = [libFile, mainFile, config, linkedPackageInMain, linkedPackageJson, linkedPackageIndex, linkedPackageOther]; - const host = createWatchedSystem(files, { currentDirectory: mainPackageRoot }); - createWatchOfConfigFile("tsconfig.json", host); - checkWatchedFilesDetailed(host, [libFile.path, mainFile.path, config.path, linkedPackageIndex.path, linkedPackageOther.path], 1); - checkWatchedDirectories(host, emptyArray, /*recursive*/ false); - checkWatchedDirectoriesDetailed(host, [`${mainPackageRoot}/@scoped`, `${mainPackageRoot}/node_modules`, linkedPackageRoot, `${mainPackageRoot}/node_modules/@types`, `${projectRoot}/node_modules/@types`], 1, /*recursive*/ true); - }); - }); - - describe("tsc-watch with custom module resolution", () => { - const projectRoot = "/user/username/projects/project"; - const configFileJson: any = { - compilerOptions: { module: "commonjs", resolveJsonModule: true }, - files: ["index.ts"] - }; - const mainFile: File = { - path: `${projectRoot}/index.ts`, - content: "import settings from './settings.json';" - }; - const config: File = { - path: `${projectRoot}/tsconfig.json`, - content: JSON.stringify(configFileJson) - }; - const settingsJson: File = { - path: `${projectRoot}/settings.json`, - content: JSON.stringify({ content: "Print this" }) - }; - - it("verify that module resolution with json extension works when returned without extension", () => { - const files = [libFile, mainFile, config, settingsJson]; - const host = createWatchedSystem(files, { currentDirectory: projectRoot }); - const compilerHost = createWatchCompilerHostOfConfigFile(config.path, {}, host); - const parsedCommandResult = parseJsonConfigFileContent(configFileJson, host, config.path); - compilerHost.resolveModuleNames = (moduleNames, containingFile) => moduleNames.map(m => { - const result = resolveModuleName(m, containingFile, parsedCommandResult.options, compilerHost); - const resolvedModule = result.resolvedModule!; - return { - resolvedFileName: resolvedModule.resolvedFileName, - isExternalLibraryImport: resolvedModule.isExternalLibraryImport, - originalFileName: resolvedModule.originalPath, - }; - }); - const watch = createWatchProgram(compilerHost); - const program = watch.getCurrentProgram().getProgram(); - checkProgramActualFiles(program, [mainFile.path, libFile.path, settingsJson.path]); - }); - }); } diff --git a/src/testRunner/unittests/tsserverHelpers.ts b/src/testRunner/unittests/tsserverHelpers.ts new file mode 100644 index 0000000000..5162f17d6a --- /dev/null +++ b/src/testRunner/unittests/tsserverHelpers.ts @@ -0,0 +1,679 @@ +namespace ts.projectSystem { + export import TI = server.typingsInstaller; + export import protocol = server.protocol; + export import CommandNames = server.CommandNames; + + export import TestServerHost = TestFSWithWatch.TestServerHost; + export type File = TestFSWithWatch.File; + export type SymLink = TestFSWithWatch.SymLink; + export type Folder = TestFSWithWatch.Folder; + export import createServerHost = TestFSWithWatch.createServerHost; + export import checkArray = TestFSWithWatch.checkArray; + export import libFile = TestFSWithWatch.libFile; + export import checkWatchedFiles = TestFSWithWatch.checkWatchedFiles; + export import checkWatchedFilesDetailed = TestFSWithWatch.checkWatchedFilesDetailed; + export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories; + export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed; + + //const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/; + //function mapOutputToJson(s: string) { + // return convertToObject( + // parseJsonText("json.json", s.replace(outputEventRegex, "")), + // [] + // ); + //} + + export const customTypesMap = { + path: "/typesMap.json", + content: `{ + "typesMap": { + "jquery": { + "match": "jquery(-(\\\\.?\\\\d+)+)?(\\\\.intellisense)?(\\\\.min)?\\\\.js$", + "types": ["jquery"] + }, + "quack": { + "match": "/duckquack-(\\\\d+)\\\\.min\\\\.js", + "types": ["duck-types"] + } + }, + "simpleMap": { + "Bacon": "baconjs", + "bliss": "blissfuljs", + "commander": "commander", + "cordova": "cordova", + "react": "react", + "lodash": "lodash" + } + }` + }; + + export interface PostExecAction { + readonly success: boolean; + readonly callback: TI.RequestCompletedAction; + } + + export const nullLogger: server.Logger = { + close: noop, + hasLevel: () => false, + loggingEnabled: () => false, + perftrc: noop, + info: noop, + msg: noop, + startGroup: noop, + endGroup: noop, + getLogFileName: () => undefined, + }; + + export function createHasErrorMessageLogger() { + let hasErrorMsg = false; + const { close, hasLevel, loggingEnabled, startGroup, endGroup, info, getLogFileName, perftrc } = nullLogger; + const logger: server.Logger = { + close, hasLevel, loggingEnabled, startGroup, endGroup, info, getLogFileName, perftrc, + msg: (s, type) => { + Debug.fail(`Error: ${s}, type: ${type}`); + hasErrorMsg = true; + } + }; + return { logger, hasErrorMsg: () => hasErrorMsg }; + } + + export class TestTypingsInstaller extends TI.TypingsInstaller implements server.ITypingsInstaller { + protected projectService!: server.ProjectService; + constructor( + readonly globalTypingsCacheLocation: string, + throttleLimit: number, + installTypingHost: server.ServerHost, + readonly typesRegistry = createMap>(), + log?: TI.Log) { + super(installTypingHost, globalTypingsCacheLocation, TestFSWithWatch.safeList.path, customTypesMap.path, throttleLimit, log); + } + + protected postExecActions: PostExecAction[] = []; + + isKnownTypesPackageName = notImplemented; + installPackage = notImplemented; + inspectValue = notImplemented; + + executePendingCommands() { + const actionsToRun = this.postExecActions; + this.postExecActions = []; + for (const action of actionsToRun) { + action.callback(action.success); + } + } + + checkPendingCommands(expectedCount: number) { + assert.equal(this.postExecActions.length, expectedCount, `Expected ${expectedCount} post install actions`); + } + + onProjectClosed = noop; + + attach(projectService: server.ProjectService) { + this.projectService = projectService; + } + + getInstallTypingHost() { + return this.installTypingHost; + } + + installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { + this.addPostExecAction("success", cb); + } + + sendResponse(response: server.SetTypings | server.InvalidateCachedTypings) { + this.projectService.updateTypingsForProject(response); + } + + enqueueInstallTypingsRequest(project: server.Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray) { + const request = server.createInstallTypingsRequest(project, typeAcquisition, unresolvedImports, this.globalTypingsCacheLocation); + this.install(request); + } + + addPostExecAction(stdout: string | string[], cb: TI.RequestCompletedAction) { + const out = isString(stdout) ? stdout : createNpmPackageJsonString(stdout); + const action: PostExecAction = { + success: !!out, + callback: cb + }; + this.postExecActions.push(action); + } + } + + function createNpmPackageJsonString(installedTypings: string[]): string { + const dependencies: MapLike = {}; + for (const typing of installedTypings) { + dependencies[typing] = "1.0.0"; + } + return JSON.stringify({ dependencies }); + } + + export function createTypesRegistry(...list: string[]): Map> { + const versionMap = { + "latest": "1.3.0", + "ts2.0": "1.0.0", + "ts2.1": "1.0.0", + "ts2.2": "1.2.0", + "ts2.3": "1.3.0", + "ts2.4": "1.3.0", + "ts2.5": "1.3.0", + "ts2.6": "1.3.0", + "ts2.7": "1.3.0" + }; + const map = createMap>(); + for (const l of list) { + map.set(l, versionMap); + } + return map; + } + + export function toExternalFile(fileName: string): protocol.ExternalFile { + return { fileName }; + } + + export function toExternalFiles(fileNames: string[]) { + return map(fileNames, toExternalFile); + } + + export function fileStats(nonZeroStats: Partial): server.FileStats { + return { ts: 0, tsSize: 0, tsx: 0, tsxSize: 0, dts: 0, dtsSize: 0, js: 0, jsSize: 0, jsx: 0, jsxSize: 0, deferred: 0, deferredSize: 0, ...nonZeroStats }; + } + + export interface ConfigFileDiagnostic { + fileName: string | undefined; + start: number | undefined; + length: number | undefined; + messageText: string; + category: DiagnosticCategory; + code: number; + reportsUnnecessary?: {}; + source?: string; + relatedInformation?: DiagnosticRelatedInformation[]; + } + + export class TestServerEventManager { + private events: server.ProjectServiceEvent[] = []; + readonly session: TestSession; + readonly service: server.ProjectService; + readonly host: TestServerHost; + constructor(files: File[], suppressDiagnosticEvents?: boolean) { + this.host = createServerHost(files); + this.session = createSession(this.host, { + canUseEvents: true, + eventHandler: event => this.events.push(event), + suppressDiagnosticEvents, + }); + this.service = this.session.getProjectService(); + } + + getEvents(): ReadonlyArray { + const events = this.events; + this.events = []; + return events; + } + + getEvent(eventName: T["eventName"]): T["data"] { + let eventData: T["data"] | undefined; + filterMutate(this.events, e => { + if (e.eventName === eventName) { + if (eventData !== undefined) { + assert(false, "more than one event found"); + } + eventData = e.data; + return false; + } + return true; + }); + return Debug.assertDefined(eventData); + } + + hasZeroEvent(eventName: T["eventName"]) { + this.events.forEach(event => assert.notEqual(event.eventName, eventName)); + } + + checkSingleConfigFileDiagEvent(configFileName: string, triggerFile: string, errors: ReadonlyArray) { + const eventData = this.getEvent(server.ConfigFileDiagEvent); + assert.equal(eventData.configFileName, configFileName); + assert.equal(eventData.triggerFile, triggerFile); + const actual = eventData.diagnostics.map(({ file, messageText, ...rest }) => ({ fileName: file && file.fileName, messageText: isString(messageText) ? messageText : "", ...rest })); + if (errors) { + assert.deepEqual(actual, errors); + } + } + + assertProjectInfoTelemetryEvent(partial: Partial, configFile = "/tsconfig.json"): void { + assert.deepEqual(this.getEvent(server.ProjectInfoTelemetryEvent), { + projectId: sys.createSHA256Hash!(configFile), + fileStats: fileStats({ ts: 1 }), + compilerOptions: {}, + extends: false, + files: false, + include: false, + exclude: false, + compileOnSave: false, + typeAcquisition: { + enable: false, + exclude: false, + include: false, + }, + configFileName: "tsconfig.json", + projectType: "configured", + languageServiceEnabled: true, + version, + ...partial, + }); + } + + assertOpenFileTelemetryEvent(info: server.OpenFileInfo): void { + assert.deepEqual(this.getEvent(server.OpenFileInfoTelemetryEvent), { info }); + } + assertNoOpenFilesTelemetryEvent(): void { + this.hasZeroEvent(server.OpenFileInfoTelemetryEvent); + } + } + + export class TestSession extends server.Session { + private seq = 0; + public events: protocol.Event[] = []; + public host!: TestServerHost; + + getProjectService() { + return this.projectService; + } + + public getSeq() { + return this.seq; + } + + public getNextSeq() { + return this.seq + 1; + } + + public executeCommandSeq(request: Partial) { + this.seq++; + request.seq = this.seq; + request.type = "request"; + return this.executeCommand(request); + } + + public event(body: T, eventName: string) { + this.events.push(server.toEvent(eventName, body)); + super.event(body, eventName); + } + + public clearMessages() { + clear(this.events); + this.host.clearOutput(); + } + } + + export function createSession(host: server.ServerHost, opts: Partial = {}) { + if (opts.typingsInstaller === undefined) { + opts.typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/ 5, host); + } + + if (opts.eventHandler !== undefined) { + opts.canUseEvents = true; + } + + const sessionOptions: server.SessionOptions = { + host, + cancellationToken: server.nullCancellationToken, + useSingleInferredProject: false, + useInferredProjectPerProjectRoot: false, + typingsInstaller: undefined!, // TODO: GH#18217 + byteLength: Utils.byteLength, + hrtime: process.hrtime, + logger: opts.logger || createHasErrorMessageLogger().logger, + canUseEvents: false + }; + + return new TestSession({ ...sessionOptions, ...opts }); + } + + //function createSessionWithEventTracking(host: server.ServerHost, eventName: T["eventName"], ...eventNames: T["eventName"][]) { + // const events: T[] = []; + // const session = createSession(host, { + // eventHandler: e => { + // if (e.eventName === eventName || eventNames.some(eventName => e.eventName === eventName)) { + // events.push(e as T); + // } + // } + // }); + + // return { session, events }; + //} + + //function createSessionWithDefaultEventHandler(host: TestServerHost, eventNames: T["event"] | T["event"][], opts: Partial = {}) { + // const session = createSession(host, { canUseEvents: true, ...opts }); + + // return { + // session, + // getEvents, + // clearEvents + // }; + + // function getEvents() { + // return mapDefined(host.getOutput(), s => { + // const e = mapOutputToJson(s); + // return (isArray(eventNames) ? eventNames.some(eventName => e.event === eventName) : e.event === eventNames) ? e as T : undefined; + // }); + // } + + // function clearEvents() { + // session.clearMessages(); + // } + //} + + export interface CreateProjectServiceParameters { + cancellationToken?: HostCancellationToken; + logger?: server.Logger; + useSingleInferredProject?: boolean; + typingsInstaller?: server.ITypingsInstaller; + eventHandler?: server.ProjectServiceEventHandler; + } + + export class TestProjectService extends server.ProjectService { + constructor(host: server.ServerHost, logger: server.Logger, cancellationToken: HostCancellationToken, useSingleInferredProject: boolean, + typingsInstaller: server.ITypingsInstaller, eventHandler: server.ProjectServiceEventHandler, opts: Partial = {}) { + super({ + host, + logger, + cancellationToken, + useSingleInferredProject, + useInferredProjectPerProjectRoot: false, + typingsInstaller, + typesMapLocation: customTypesMap.path, + eventHandler, + ...opts + }); + } + + checkNumberOfProjects(count: { inferredProjects?: number, configuredProjects?: number, externalProjects?: number }) { + checkNumberOfProjects(this, count); + } + } + export function createProjectService(host: server.ServerHost, parameters: CreateProjectServiceParameters = {}, options?: Partial) { + const cancellationToken = parameters.cancellationToken || server.nullCancellationToken; + const logger = parameters.logger || createHasErrorMessageLogger().logger; + const useSingleInferredProject = parameters.useSingleInferredProject !== undefined ? parameters.useSingleInferredProject : false; + return new TestProjectService(host, logger, cancellationToken, useSingleInferredProject, parameters.typingsInstaller!, parameters.eventHandler!, options); // TODO: GH#18217 + } + + export function checkNumberOfConfiguredProjects(projectService: server.ProjectService, expected: number) { + assert.equal(projectService.configuredProjects.size, expected, `expected ${expected} configured project(s)`); + } + + export function checkNumberOfExternalProjects(projectService: server.ProjectService, expected: number) { + assert.equal(projectService.externalProjects.length, expected, `expected ${expected} external project(s)`); + } + + export function checkNumberOfInferredProjects(projectService: server.ProjectService, expected: number) { + assert.equal(projectService.inferredProjects.length, expected, `expected ${expected} inferred project(s)`); + } + + export function checkNumberOfProjects(projectService: server.ProjectService, count: { inferredProjects?: number, configuredProjects?: number, externalProjects?: number }) { + checkNumberOfConfiguredProjects(projectService, count.configuredProjects || 0); + checkNumberOfExternalProjects(projectService, count.externalProjects || 0); + checkNumberOfInferredProjects(projectService, count.inferredProjects || 0); + } + + export function configuredProjectAt(projectService: server.ProjectService, index: number) { + const values = projectService.configuredProjects.values(); + while (index > 0) { + values.next(); + index--; + } + return values.next().value; + } + + export function checkProjectActualFiles(project: server.Project, expectedFiles: ReadonlyArray) { + checkArray(`${server.ProjectKind[project.projectKind]} project, actual files`, project.getFileNames(), expectedFiles); + } + + //function checkProjectRootFiles(project: server.Project, expectedFiles: ReadonlyArray) { + // checkArray(`${server.ProjectKind[project.projectKind]} project, rootFileNames`, project.getRootFiles(), expectedFiles); + //} + + export function mapCombinedPathsInAncestor(dir: string, path2: string, mapAncestor: (ancestor: string) => boolean) { + dir = normalizePath(dir); + const result: string[] = []; + forEachAncestorDirectory(dir, ancestor => { + if (mapAncestor(ancestor)) { + result.push(combinePaths(ancestor, path2)); + } + }); + return result; + } + + export function getRootsToWatchWithAncestorDirectory(dir: string, path2: string) { + return mapCombinedPathsInAncestor(dir, path2, ancestor => ancestor.split(directorySeparator).length > 4); + } + + export const nodeModules = "node_modules"; + //function getNodeModuleDirectories(dir: string) { + // return getRootsToWatchWithAncestorDirectory(dir, nodeModules); + //} + + export const nodeModulesAtTypes = "node_modules/@types"; + export function getTypeRootsFromLocation(currentDirectory: string) { + return getRootsToWatchWithAncestorDirectory(currentDirectory, nodeModulesAtTypes); + } + + //function getNumberOfWatchesInvokedForRecursiveWatches(recursiveWatchedDirs: string[], file: string) { + // return countWhere(recursiveWatchedDirs, dir => file.length > dir.length && startsWith(file, dir) && file[dir.length] === directorySeparator); + //} + + //function checkOpenFiles(projectService: server.ProjectService, expectedFiles: File[]) { + // checkArray("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path)!.fileName), expectedFiles.map(file => file.path)); + //} + + //function protocolLocationFromSubstring(str: string, substring: string): protocol.Location { + // const start = str.indexOf(substring); + // Debug.assert(start !== -1); + // return protocolToLocation(str)(start); + //} + //function protocolToLocation(text: string): (pos: number) => protocol.Location { + // const lineStarts = computeLineStarts(text); + // return pos => { + // const x = computeLineAndCharacterOfPosition(lineStarts, pos); + // return { line: x.line + 1, offset: x.character + 1 }; + // }; + //} + //function protocolTextSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): protocol.TextSpan { + // const span = textSpanFromSubstring(str, substring, options); + // const toLocation = protocolToLocation(str); + // return { start: toLocation(span.start), end: toLocation(textSpanEnd(span)) }; + //} + //function protocolRenameSpanFromSubstring( + // str: string, + // substring: string, + // options?: SpanFromSubstringOptions, + // prefixSuffixText?: { readonly prefixText?: string, readonly suffixText?: string }, + //): protocol.RenameTextSpan { + // return { ...protocolTextSpanFromSubstring(str, substring, options), ...prefixSuffixText }; + //} + //function textSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): TextSpan { + // const start = nthIndexOf(str, substring, options ? options.index : 0); + // Debug.assert(start !== -1); + // return createTextSpan(start, substring.length); + //} + //function protocolFileLocationFromSubstring(file: File, substring: string): protocol.FileLocationRequestArgs { + // return { file: file.path, ...protocolLocationFromSubstring(file.content, substring) }; + //} + //function protocolFileSpanFromSubstring(file: File, substring: string, options?: SpanFromSubstringOptions): protocol.FileSpan { + // return { file: file.path, ...protocolTextSpanFromSubstring(file.content, substring, options) }; + //} + //function documentSpanFromSubstring(file: File, substring: string, options?: SpanFromSubstringOptions): DocumentSpan { + // return { fileName: file.path, textSpan: textSpanFromSubstring(file.content, substring, options) }; + //} + //function renameLocation(file: File, substring: string, options?: SpanFromSubstringOptions): RenameLocation { + // return documentSpanFromSubstring(file, substring, options); + //} + //interface SpanFromSubstringOptions { + // readonly index: number; + //} + + //function nthIndexOf(str: string, substr: string, n: number): number { + // let index = -1; + // for (; n >= 0; n--) { + // index = str.indexOf(substr, index + 1); + // if (index === -1) return -1; + // } + // return index; + //} + + /** + * Test server cancellation token used to mock host token cancellation requests. + * The cancelAfterRequest constructor param specifies how many isCancellationRequested() calls + * should be made before canceling the token. The id of the request to cancel should be set with + * setRequestToCancel(); + */ + export class TestServerCancellationToken implements server.ServerCancellationToken { + private currentId: number | undefined = -1; + private requestToCancel = -1; + private isCancellationRequestedCount = 0; + + constructor(private cancelAfterRequest = 0) { + } + + setRequest(requestId: number) { + this.currentId = requestId; + } + + setRequestToCancel(requestId: number) { + this.resetToken(); + this.requestToCancel = requestId; + } + + resetRequest(requestId: number) { + assert.equal(requestId, this.currentId, "unexpected request id in cancellation"); + this.currentId = undefined; + } + + isCancellationRequested() { + this.isCancellationRequestedCount++; + // If the request id is the request to cancel and isCancellationRequestedCount + // has been met then cancel the request. Ex: cancel the request if it is a + // nav bar request & isCancellationRequested() has already been called three times. + return this.requestToCancel === this.currentId && this.isCancellationRequestedCount >= this.cancelAfterRequest; + } + + resetToken() { + this.currentId = -1; + this.isCancellationRequestedCount = 0; + this.requestToCancel = -1; + } + } + + export function makeSessionRequest(command: string, args: T): protocol.Request { + return { + seq: 0, + type: "request", + command, + arguments: args + }; + } + + export function executeSessionRequest(session: server.Session, command: TRequest["command"], args: TRequest["arguments"]): TResponse["body"] { + return session.executeCommand(makeSessionRequest(command, args)).response as TResponse["body"]; + } + + export function executeSessionRequestNoResponse(session: server.Session, command: TRequest["command"], args: TRequest["arguments"]): void { + session.executeCommand(makeSessionRequest(command, args)); + } + + export function openFilesForSession(files: ReadonlyArray, session: server.Session): void { + for (const file of files) { + session.executeCommand(makeSessionRequest(CommandNames.Open, + "projectRootPath" in file ? { file: typeof file.file === "string" ? file.file : file.file.path, projectRootPath: file.projectRootPath } : { file: file.path })); + } + } + + export function closeFilesForSession(files: ReadonlyArray, session: server.Session): void { + for (const file of files) { + session.executeCommand(makeSessionRequest(CommandNames.Close, { file: file.path })); + } + } + + //interface ErrorInformation { + // diagnosticMessage: DiagnosticMessage; + // errorTextArguments?: string[]; + //} + + //function getProtocolDiagnosticMessage({ diagnosticMessage, errorTextArguments = [] }: ErrorInformation) { + // return formatStringFromArgs(diagnosticMessage.message, errorTextArguments); + //} + + //function verifyDiagnostics(actual: server.protocol.Diagnostic[], expected: ErrorInformation[]) { + // const expectedErrors = expected.map(getProtocolDiagnosticMessage); + // assert.deepEqual(actual.map(diag => flattenDiagnosticMessageText(diag.text, "\n")), expectedErrors); + //} + + //function verifyNoDiagnostics(actual: server.protocol.Diagnostic[]) { + // verifyDiagnostics(actual, []); + //} + + //function checkErrorMessage(session: TestSession, eventName: protocol.DiagnosticEventKind, diagnostics: protocol.DiagnosticEventBody, isMostRecent = false): void { + // checkNthEvent(session, server.toEvent(eventName, diagnostics), 0, isMostRecent); + //} + + //function createDiagnostic(start: protocol.Location, end: protocol.Location, message: DiagnosticMessage, args: ReadonlyArray = [], category = diagnosticCategoryName(message), reportsUnnecessary?: {}, relatedInformation?: protocol.DiagnosticRelatedInformation[]): protocol.Diagnostic { + // return { start, end, text: formatStringFromArgs(message.message, args), code: message.code, category, reportsUnnecessary, relatedInformation, source: undefined }; + //} + + //function checkCompleteEvent(session: TestSession, numberOfCurrentEvents: number, expectedSequenceId: number, isMostRecent = true): void { + // checkNthEvent(session, server.toEvent("requestCompleted", { request_seq: expectedSequenceId }), numberOfCurrentEvents - 1, isMostRecent); + //} + + //function checkProjectUpdatedInBackgroundEvent(session: TestSession, openFiles: string[]) { + // checkNthEvent(session, server.toEvent("projectsUpdatedInBackground", { openFiles }), 0, /*isMostRecent*/ true); + //} + + //function checkNoDiagnosticEvents(session: TestSession) { + // for (const event of session.events) { + // assert.isFalse(event.event.endsWith("Diag"), JSON.stringify(event)); + // } + //} + + //function checkNthEvent(session: TestSession, expectedEvent: protocol.Event, index: number, isMostRecent: boolean) { + // const events = session.events; + // assert.deepEqual(events[index], expectedEvent, `Expected ${JSON.stringify(expectedEvent)} at ${index} in ${JSON.stringify(events)}`); + + // const outputs = session.host.getOutput(); + // assert.equal(outputs[index], server.formatMessage(expectedEvent, nullLogger, Utils.byteLength, session.host.newLine)); + + // if (isMostRecent) { + // assert.strictEqual(events.length, index + 1, JSON.stringify(events)); + // assert.strictEqual(outputs.length, index + 1, JSON.stringify(outputs)); + // } + //} + + //function makeReferenceItem(file: File, isDefinition: boolean, text: string, lineText: string, options?: SpanFromSubstringOptions): protocol.ReferencesResponseItem { + // return { + // ...protocolFileSpanFromSubstring(file, text, options), + // isDefinition, + // isWriteAccess: isDefinition, + // lineText, + // }; + //} + + //function makeReferenceEntry(file: File, isDefinition: boolean, text: string, options?: SpanFromSubstringOptions): ReferenceEntry { + // return { + // ...documentSpanFromSubstring(file, text, options), + // isDefinition, + // isWriteAccess: isDefinition, + // isInString: undefined, + // }; + //} + + //function checkDeclarationFiles(file: File, session: TestSession, expectedFiles: ReadonlyArray): void { + // openFilesForSession([file], session); + // const project = Debug.assertDefined(session.getProjectService().getDefaultProjectForFile(file.path as server.NormalizedPath, /*ensureProject*/ false)); + // const program = project.getCurrentProgram()!; + // const output = getFileEmitOutput(program, Debug.assertDefined(program.getSourceFile(file.path)), /*emitOnlyDtsFiles*/ true); + // closeFilesForSession([file], session); + + // Debug.assert(!output.emitSkipped); + // assert.deepEqual(output.outputFiles, expectedFiles.map((e): OutputFile => ({ name: e.path, text: e.content, writeByteOrderMark: false }))); + //} +} diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index cefa8a5e34..3b7c276cfc 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -1,21 +1,4 @@ namespace ts.projectSystem { - import TI = server.typingsInstaller; - import protocol = server.protocol; - import CommandNames = server.CommandNames; - - export import TestServerHost = TestFSWithWatch.TestServerHost; - export type File = TestFSWithWatch.File; - export type SymLink = TestFSWithWatch.SymLink; - export type Folder = TestFSWithWatch.Folder; - export import createServerHost = TestFSWithWatch.createServerHost; - export import checkArray = TestFSWithWatch.checkArray; - export import libFile = TestFSWithWatch.libFile; - export import checkWatchedFiles = TestFSWithWatch.checkWatchedFiles; - export import checkWatchedFilesDetailed = TestFSWithWatch.checkWatchedFilesDetailed; - export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories; - export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed; - import safeList = TestFSWithWatch.safeList; - const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/; function mapOutputToJson(s: string) { return convertToObject( @@ -24,319 +7,6 @@ namespace ts.projectSystem { ); } - export const customTypesMap = { - path: "/typesMap.json", - content: `{ - "typesMap": { - "jquery": { - "match": "jquery(-(\\\\.?\\\\d+)+)?(\\\\.intellisense)?(\\\\.min)?\\\\.js$", - "types": ["jquery"] - }, - "quack": { - "match": "/duckquack-(\\\\d+)\\\\.min\\\\.js", - "types": ["duck-types"] - } - }, - "simpleMap": { - "Bacon": "baconjs", - "bliss": "blissfuljs", - "commander": "commander", - "cordova": "cordova", - "react": "react", - "lodash": "lodash" - } - }` - }; - - export interface PostExecAction { - readonly success: boolean; - readonly callback: TI.RequestCompletedAction; - } - - export const nullLogger: server.Logger = { - close: noop, - hasLevel: () => false, - loggingEnabled: () => false, - perftrc: noop, - info: noop, - msg: noop, - startGroup: noop, - endGroup: noop, - getLogFileName: () => undefined, - }; - - export function createHasErrorMessageLogger() { - let hasErrorMsg = false; - const { close, hasLevel, loggingEnabled, startGroup, endGroup, info, getLogFileName, perftrc } = nullLogger; - const logger: server.Logger = { - close, hasLevel, loggingEnabled, startGroup, endGroup, info, getLogFileName, perftrc, - msg: (s, type) => { - Debug.fail(`Error: ${s}, type: ${type}`); - hasErrorMsg = true; - } - }; - return { logger, hasErrorMsg: () => hasErrorMsg }; - } - - export class TestTypingsInstaller extends TI.TypingsInstaller implements server.ITypingsInstaller { - protected projectService!: server.ProjectService; - constructor( - readonly globalTypingsCacheLocation: string, - throttleLimit: number, - installTypingHost: server.ServerHost, - readonly typesRegistry = createMap>(), - log?: TI.Log) { - super(installTypingHost, globalTypingsCacheLocation, safeList.path, customTypesMap.path, throttleLimit, log); - } - - protected postExecActions: PostExecAction[] = []; - - isKnownTypesPackageName = notImplemented; - installPackage = notImplemented; - inspectValue = notImplemented; - - executePendingCommands() { - const actionsToRun = this.postExecActions; - this.postExecActions = []; - for (const action of actionsToRun) { - action.callback(action.success); - } - } - - checkPendingCommands(expectedCount: number) { - assert.equal(this.postExecActions.length, expectedCount, `Expected ${expectedCount} post install actions`); - } - - onProjectClosed = noop; - - attach(projectService: server.ProjectService) { - this.projectService = projectService; - } - - getInstallTypingHost() { - return this.installTypingHost; - } - - installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { - this.addPostExecAction("success", cb); - } - - sendResponse(response: server.SetTypings | server.InvalidateCachedTypings) { - this.projectService.updateTypingsForProject(response); - } - - enqueueInstallTypingsRequest(project: server.Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray) { - const request = server.createInstallTypingsRequest(project, typeAcquisition, unresolvedImports, this.globalTypingsCacheLocation); - this.install(request); - } - - addPostExecAction(stdout: string | string[], cb: TI.RequestCompletedAction) { - const out = isString(stdout) ? stdout : createNpmPackageJsonString(stdout); - const action: PostExecAction = { - success: !!out, - callback: cb - }; - this.postExecActions.push(action); - } - } - - function createNpmPackageJsonString(installedTypings: string[]): string { - const dependencies: MapLike = {}; - for (const typing of installedTypings) { - dependencies[typing] = "1.0.0"; - } - return JSON.stringify({ dependencies }); - } - - export function createTypesRegistry(...list: string[]): Map> { - const versionMap = { - "latest": "1.3.0", - "ts2.0": "1.0.0", - "ts2.1": "1.0.0", - "ts2.2": "1.2.0", - "ts2.3": "1.3.0", - "ts2.4": "1.3.0", - "ts2.5": "1.3.0", - "ts2.6": "1.3.0", - "ts2.7": "1.3.0" - }; - const map = createMap>(); - for (const l of list) { - map.set(l, versionMap); - } - return map; - } - - function createHostModuleResolutionTrace(host: TestServerHost & ModuleResolutionHost) { - const resolutionTrace: string[] = []; - host.trace = resolutionTrace.push.bind(resolutionTrace); - return resolutionTrace; - } - - export function toExternalFile(fileName: string): protocol.ExternalFile { - return { fileName }; - } - - export function toExternalFiles(fileNames: string[]) { - return map(fileNames, toExternalFile); - } - - export function fileStats(nonZeroStats: Partial): server.FileStats { - return { ts: 0, tsSize: 0, tsx: 0, tsxSize: 0, dts: 0, dtsSize: 0, js: 0, jsSize: 0, jsx: 0, jsxSize: 0, deferred: 0, deferredSize: 0, ...nonZeroStats }; - } - - export interface ConfigFileDiagnostic { - fileName: string | undefined; - start: number | undefined; - length: number | undefined; - messageText: string; - category: DiagnosticCategory; - code: number; - reportsUnnecessary?: {}; - source?: string; - relatedInformation?: DiagnosticRelatedInformation[]; - } - - export class TestServerEventManager { - private events: server.ProjectServiceEvent[] = []; - readonly session: TestSession; - readonly service: server.ProjectService; - readonly host: TestServerHost; - constructor(files: File[], suppressDiagnosticEvents?: boolean) { - this.host = createServerHost(files); - this.session = createSession(this.host, { - canUseEvents: true, - eventHandler: event => this.events.push(event), - suppressDiagnosticEvents, - }); - this.service = this.session.getProjectService(); - } - - getEvents(): ReadonlyArray { - const events = this.events; - this.events = []; - return events; - } - - getEvent(eventName: T["eventName"]): T["data"] { - let eventData: T["data"] | undefined; - filterMutate(this.events, e => { - if (e.eventName === eventName) { - if (eventData !== undefined) { - assert(false, "more than one event found"); - } - eventData = e.data; - return false; - } - return true; - }); - return Debug.assertDefined(eventData); - } - - hasZeroEvent(eventName: T["eventName"]) { - this.events.forEach(event => assert.notEqual(event.eventName, eventName)); - } - - checkSingleConfigFileDiagEvent(configFileName: string, triggerFile: string, errors: ReadonlyArray) { - const eventData = this.getEvent(server.ConfigFileDiagEvent); - assert.equal(eventData.configFileName, configFileName); - assert.equal(eventData.triggerFile, triggerFile); - const actual = eventData.diagnostics.map(({ file, messageText, ...rest }) => ({ fileName: file && file.fileName, messageText: isString(messageText) ? messageText : "", ...rest })); - if (errors) { - assert.deepEqual(actual, errors); - } - } - - assertProjectInfoTelemetryEvent(partial: Partial, configFile = "/tsconfig.json"): void { - assert.deepEqual(this.getEvent(server.ProjectInfoTelemetryEvent), { - projectId: sys.createSHA256Hash!(configFile), - fileStats: fileStats({ ts: 1 }), - compilerOptions: {}, - extends: false, - files: false, - include: false, - exclude: false, - compileOnSave: false, - typeAcquisition: { - enable: false, - exclude: false, - include: false, - }, - configFileName: "tsconfig.json", - projectType: "configured", - languageServiceEnabled: true, - version, - ...partial, - }); - } - - assertOpenFileTelemetryEvent(info: server.OpenFileInfo): void { - assert.deepEqual(this.getEvent(server.OpenFileInfoTelemetryEvent), { info }); - } - assertNoOpenFilesTelemetryEvent(): void { - this.hasZeroEvent(server.OpenFileInfoTelemetryEvent); - } - } - - class TestSession extends server.Session { - private seq = 0; - public events: protocol.Event[] = []; - public host!: TestServerHost; - - getProjectService() { - return this.projectService; - } - - public getSeq() { - return this.seq; - } - - public getNextSeq() { - return this.seq + 1; - } - - public executeCommandSeq(request: Partial) { - this.seq++; - request.seq = this.seq; - request.type = "request"; - return this.executeCommand(request); - } - - public event(body: T, eventName: string) { - this.events.push(server.toEvent(eventName, body)); - super.event(body, eventName); - } - - public clearMessages() { - clear(this.events); - this.host.clearOutput(); - } - } - - export function createSession(host: server.ServerHost, opts: Partial = {}) { - if (opts.typingsInstaller === undefined) { - opts.typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/ 5, host); - } - - if (opts.eventHandler !== undefined) { - opts.canUseEvents = true; - } - - const sessionOptions: server.SessionOptions = { - host, - cancellationToken: server.nullCancellationToken, - useSingleInferredProject: false, - useInferredProjectPerProjectRoot: false, - typingsInstaller: undefined!, // TODO: GH#18217 - byteLength: Utils.byteLength, - hrtime: process.hrtime, - logger: opts.logger || createHasErrorMessageLogger().logger, - canUseEvents: false - }; - - return new TestSession({ ...sessionOptions, ...opts }); - } - function createSessionWithEventTracking(host: server.ServerHost, eventName: T["eventName"], ...eventNames: T["eventName"][]) { const events: T[] = []; const session = createSession(host, { @@ -371,101 +41,14 @@ namespace ts.projectSystem { } } - interface CreateProjectServiceParameters { - cancellationToken?: HostCancellationToken; - logger?: server.Logger; - useSingleInferredProject?: boolean; - typingsInstaller?: server.ITypingsInstaller; - eventHandler?: server.ProjectServiceEventHandler; - } - - export class TestProjectService extends server.ProjectService { - constructor(host: server.ServerHost, logger: server.Logger, cancellationToken: HostCancellationToken, useSingleInferredProject: boolean, - typingsInstaller: server.ITypingsInstaller, eventHandler: server.ProjectServiceEventHandler, opts: Partial = {}) { - super({ - host, - logger, - cancellationToken, - useSingleInferredProject, - useInferredProjectPerProjectRoot: false, - typingsInstaller, - typesMapLocation: customTypesMap.path, - eventHandler, - ...opts - }); - } - - checkNumberOfProjects(count: { inferredProjects?: number, configuredProjects?: number, externalProjects?: number }) { - checkNumberOfProjects(this, count); - } - } - export function createProjectService(host: server.ServerHost, parameters: CreateProjectServiceParameters = {}, options?: Partial) { - const cancellationToken = parameters.cancellationToken || server.nullCancellationToken; - const logger = parameters.logger || createHasErrorMessageLogger().logger; - const useSingleInferredProject = parameters.useSingleInferredProject !== undefined ? parameters.useSingleInferredProject : false; - return new TestProjectService(host, logger, cancellationToken, useSingleInferredProject, parameters.typingsInstaller!, parameters.eventHandler!, options); // TODO: GH#18217 - } - - export function checkNumberOfConfiguredProjects(projectService: server.ProjectService, expected: number) { - assert.equal(projectService.configuredProjects.size, expected, `expected ${expected} configured project(s)`); - } - - function checkNumberOfExternalProjects(projectService: server.ProjectService, expected: number) { - assert.equal(projectService.externalProjects.length, expected, `expected ${expected} external project(s)`); - } - - function checkNumberOfInferredProjects(projectService: server.ProjectService, expected: number) { - assert.equal(projectService.inferredProjects.length, expected, `expected ${expected} inferred project(s)`); - } - - export function checkNumberOfProjects(projectService: server.ProjectService, count: { inferredProjects?: number, configuredProjects?: number, externalProjects?: number }) { - checkNumberOfConfiguredProjects(projectService, count.configuredProjects || 0); - checkNumberOfExternalProjects(projectService, count.externalProjects || 0); - checkNumberOfInferredProjects(projectService, count.inferredProjects || 0); - } - - export function configuredProjectAt(projectService: server.ProjectService, index: number) { - const values = projectService.configuredProjects.values(); - while (index > 0) { - values.next(); - index--; - } - return values.next().value; - } - - export function checkProjectActualFiles(project: server.Project, expectedFiles: ReadonlyArray) { - checkArray(`${server.ProjectKind[project.projectKind]} project, actual files`, project.getFileNames(), expectedFiles); - } - function checkProjectRootFiles(project: server.Project, expectedFiles: ReadonlyArray) { checkArray(`${server.ProjectKind[project.projectKind]} project, rootFileNames`, project.getRootFiles(), expectedFiles); } - function mapCombinedPathsInAncestor(dir: string, path2: string, mapAncestor: (ancestor: string) => boolean) { - dir = normalizePath(dir); - const result: string[] = []; - forEachAncestorDirectory(dir, ancestor => { - if (mapAncestor(ancestor)) { - result.push(combinePaths(ancestor, path2)); - } - }); - return result; - } - - function getRootsToWatchWithAncestorDirectory(dir: string, path2: string) { - return mapCombinedPathsInAncestor(dir, path2, ancestor => ancestor.split(directorySeparator).length > 4); - } - - const nodeModules = "node_modules"; function getNodeModuleDirectories(dir: string) { return getRootsToWatchWithAncestorDirectory(dir, nodeModules); } - export const nodeModulesAtTypes = "node_modules/@types"; - export function getTypeRootsFromLocation(currentDirectory: string) { - return getRootsToWatchWithAncestorDirectory(currentDirectory, nodeModulesAtTypes); - } - function getNumberOfWatchesInvokedForRecursiveWatches(recursiveWatchedDirs: string[], file: string) { return countWhere(recursiveWatchedDirs, dir => file.length > dir.length && startsWith(file, dir) && file[dir.length] === directorySeparator); } @@ -533,79 +116,6 @@ namespace ts.projectSystem { return index; } - /** - * Test server cancellation token used to mock host token cancellation requests. - * The cancelAfterRequest constructor param specifies how many isCancellationRequested() calls - * should be made before canceling the token. The id of the request to cancel should be set with - * setRequestToCancel(); - */ - export class TestServerCancellationToken implements server.ServerCancellationToken { - private currentId: number | undefined = -1; - private requestToCancel = -1; - private isCancellationRequestedCount = 0; - - constructor(private cancelAfterRequest = 0) { - } - - setRequest(requestId: number) { - this.currentId = requestId; - } - - setRequestToCancel(requestId: number) { - this.resetToken(); - this.requestToCancel = requestId; - } - - resetRequest(requestId: number) { - assert.equal(requestId, this.currentId, "unexpected request id in cancellation"); - this.currentId = undefined; - } - - isCancellationRequested() { - this.isCancellationRequestedCount++; - // If the request id is the request to cancel and isCancellationRequestedCount - // has been met then cancel the request. Ex: cancel the request if it is a - // nav bar request & isCancellationRequested() has already been called three times. - return this.requestToCancel === this.currentId && this.isCancellationRequestedCount >= this.cancelAfterRequest; - } - - resetToken() { - this.currentId = -1; - this.isCancellationRequestedCount = 0; - this.requestToCancel = -1; - } - } - - export function makeSessionRequest(command: string, args: T): protocol.Request { - return { - seq: 0, - type: "request", - command, - arguments: args - }; - } - - export function executeSessionRequest(session: server.Session, command: TRequest["command"], args: TRequest["arguments"]): TResponse["body"] { - return session.executeCommand(makeSessionRequest(command, args)).response as TResponse["body"]; - } - - export function executeSessionRequestNoResponse(session: server.Session, command: TRequest["command"], args: TRequest["arguments"]): void { - session.executeCommand(makeSessionRequest(command, args)); - } - - export function openFilesForSession(files: ReadonlyArray, session: server.Session): void { - for (const file of files) { - session.executeCommand(makeSessionRequest(CommandNames.Open, - "projectRootPath" in file ? { file: typeof file.file === "string" ? file.file : file.file.path, projectRootPath: file.projectRootPath } : { file: file.path })); - } - } - - export function closeFilesForSession(files: ReadonlyArray, session: server.Session): void { - for (const file of files) { - session.executeCommand(makeSessionRequest(CommandNames.Close, { file: file.path })); - } - } - interface ErrorInformation { diagnosticMessage: DiagnosticMessage; errorTextArguments?: string[]; @@ -4171,47 +3681,6 @@ var x = 10;` }); }); - describe("tsserverProjectSystem extra resolution pass in lshost", () => { - it("can load typings that are proper modules", () => { - const file1 = { - path: "/a/b/app.js", - content: `var x = require("lib")` - }; - const lib = { - path: "/a/cache/node_modules/@types/lib/index.d.ts", - content: "export let x = 1" - }; - const host: TestServerHost & ModuleResolutionHost = createServerHost([file1, lib]); - const resolutionTrace = createHostModuleResolutionTrace(host); - const projectService = createProjectService(host, { typingsInstaller: new TestTypingsInstaller("/a/cache", /*throttleLimit*/5, host) }); - - projectService.setCompilerOptionsForInferredProjects({ traceResolution: true, allowJs: true }); - projectService.openClientFile(file1.path); - projectService.checkNumberOfProjects({ inferredProjects: 1 }); - const proj = projectService.inferredProjects[0]; - - assert.deepEqual(resolutionTrace, [ - "======== Resolving module 'lib' from '/a/b/app.js'. ========", - "Module resolution kind is not specified, using 'NodeJs'.", - "Loading module 'lib' from 'node_modules' folder, target file type 'TypeScript'.", - "Directory '/a/b/node_modules' does not exist, skipping all lookups in it.", - "Directory '/a/node_modules' does not exist, skipping all lookups in it.", - "Directory '/node_modules' does not exist, skipping all lookups in it.", - "Loading module 'lib' from 'node_modules' folder, target file type 'JavaScript'.", - "Directory '/a/b/node_modules' does not exist, skipping all lookups in it.", - "Directory '/a/node_modules' does not exist, skipping all lookups in it.", - "Directory '/node_modules' does not exist, skipping all lookups in it.", - "======== Module name 'lib' was not resolved. ========", - `Auto discovery for typings is enabled in project '${proj.getProjectName()}'. Running extra resolution pass for module 'lib' using cache location '/a/cache'.`, - "File '/a/cache/node_modules/lib.d.ts' does not exist.", - "File '/a/cache/node_modules/@types/lib/package.json' does not exist.", - "File '/a/cache/node_modules/@types/lib.d.ts' does not exist.", - "File '/a/cache/node_modules/@types/lib/index.d.ts' exist - use it as a name resolution result.", - ]); - checkProjectActualFiles(proj, [file1.path, lib.path]); - }); - }); - describe("tsserverProjectSystem navigate-to for javascript project", () => { function containsNavToItem(items: protocol.NavtoItem[], itemName: string, itemKind: string) { return find(items, item => item.name === itemName && item.kind === itemKind) !== undefined; @@ -8218,56 +7687,6 @@ var x = 10;` }); }); - describe("tsserverProjectSystem Watched recursive directories with windows style file system", () => { - function verifyWatchedDirectories(rootedPath: string, useProjectAtRoot: boolean) { - const root = useProjectAtRoot ? rootedPath : `${rootedPath}myfolder/allproject/`; - const configFile: File = { - path: root + "project/tsconfig.json", - content: "{}" - }; - const file1: File = { - path: root + "project/file1.ts", - content: "let x = 10;" - }; - const file2: File = { - path: root + "project/file2.ts", - content: "let y = 10;" - }; - const files = [configFile, file1, file2, libFile]; - const host = createServerHost(files, { useWindowsStylePaths: true }); - const projectService = createProjectService(host); - projectService.openClientFile(file1.path); - const project = projectService.configuredProjects.get(configFile.path)!; - assert.isDefined(project); - const winsowsStyleLibFilePath = "c:/" + libFile.path.substring(1); - checkProjectActualFiles(project, files.map(f => f === libFile ? winsowsStyleLibFilePath : f.path)); - checkWatchedFiles(host, mapDefined(files, f => f === libFile ? winsowsStyleLibFilePath : f === file1 ? undefined : f.path)); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, [ - root + "project", - root + "project/node_modules/@types" - ].concat(useProjectAtRoot ? [] : [root + nodeModulesAtTypes]), /*recursive*/ true); - } - - function verifyRootedDirectoryWatch(rootedPath: string) { - it("When project is in rootFolder of style c:/", () => { - verifyWatchedDirectories(rootedPath, /*useProjectAtRoot*/ true); - }); - - it("When files at some folder other than root", () => { - verifyWatchedDirectories(rootedPath, /*useProjectAtRoot*/ false); - }); - } - - describe("for rootFolder of style c:/", () => { - verifyRootedDirectoryWatch("c:/"); - }); - - describe("for rootFolder of style c:/users/username", () => { - verifyRootedDirectoryWatch("c:/users/username/"); - }); - }); - describe("tsserverProjectSystem typingsInstaller on inferred Project", () => { it("when projectRootPath is provided", () => { const projects = "/users/username/projects"; @@ -8671,490 +8090,6 @@ new C();` }); }); - describe("tsserverProjectSystem module resolution caching", () => { - const projectLocation = "/user/username/projects/myproject"; - const configFile: File = { - path: `${projectLocation}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { traceResolution: true } }) - }; - - function getModules(module1Path: string, module2Path: string) { - const module1: File = { - path: module1Path, - content: `export function module1() {}` - }; - const module2: File = { - path: module2Path, - content: `export function module2() {}` - }; - return { module1, module2 }; - } - - function verifyTrace(resolutionTrace: string[], expected: string[]) { - assert.deepEqual(resolutionTrace, expected); - resolutionTrace.length = 0; - } - - function getExpectedFileDoesNotExistResolutionTrace(host: TestServerHost, expectedTrace: string[], foundModule: boolean, module: File, directory: string, file: string, ignoreIfParentMissing?: boolean) { - if (!foundModule) { - const path = combinePaths(directory, file); - if (!ignoreIfParentMissing || host.directoryExists(getDirectoryPath(path))) { - if (module.path === path) { - foundModule = true; - } - else { - expectedTrace.push(`File '${path}' does not exist.`); - } - } - } - return foundModule; - } - - function getExpectedMissedLocationResolutionTrace(host: TestServerHost, expectedTrace: string[], dirPath: string, module: File, moduleName: string, useNodeModules: boolean, cacheLocation?: string) { - let foundModule = false; - forEachAncestorDirectory(dirPath, dirPath => { - if (dirPath === cacheLocation) { - return foundModule; - } - - const directory = useNodeModules ? combinePaths(dirPath, nodeModules) : dirPath; - if (useNodeModules && !foundModule && !host.directoryExists(directory)) { - expectedTrace.push(`Directory '${directory}' does not exist, skipping all lookups in it.`); - return undefined; - } - foundModule = getExpectedFileDoesNotExistResolutionTrace(host, expectedTrace, foundModule, module, directory, `${moduleName}/package.json`, /*ignoreIfParentMissing*/ true); - foundModule = getExpectedFileDoesNotExistResolutionTrace(host, expectedTrace, foundModule, module, directory, `${moduleName}.ts`); - foundModule = getExpectedFileDoesNotExistResolutionTrace(host, expectedTrace, foundModule, module, directory, `${moduleName}.tsx`); - foundModule = getExpectedFileDoesNotExistResolutionTrace(host, expectedTrace, foundModule, module, directory, `${moduleName}.d.ts`); - foundModule = getExpectedFileDoesNotExistResolutionTrace(host, expectedTrace, foundModule, module, directory, `${moduleName}/index.ts`, /*ignoreIfParentMissing*/ true); - if (useNodeModules && !foundModule) { - expectedTrace.push(`Directory '${directory}/@types' does not exist, skipping all lookups in it.`); - } - return foundModule ? true : undefined; - }); - } - - function getExpectedResolutionTraceHeader(expectedTrace: string[], file: File, moduleName: string) { - expectedTrace.push( - `======== Resolving module '${moduleName}' from '${file.path}'. ========`, - `Module resolution kind is not specified, using 'NodeJs'.` - ); - } - - function getExpectedResolutionTraceFooter(expectedTrace: string[], module: File, moduleName: string, addRealPathTrace: boolean, ignoreModuleFileFound?: boolean) { - if (!ignoreModuleFileFound) { - expectedTrace.push(`File '${module.path}' exist - use it as a name resolution result.`); - } - if (addRealPathTrace) { - expectedTrace.push(`Resolving real path for '${module.path}', result '${module.path}'.`); - } - expectedTrace.push(`======== Module name '${moduleName}' was successfully resolved to '${module.path}'. ========`); - } - - function getExpectedRelativeModuleResolutionTrace(host: TestServerHost, file: File, module: File, moduleName: string, expectedTrace: string[] = []) { - getExpectedResolutionTraceHeader(expectedTrace, file, moduleName); - expectedTrace.push(`Loading module as file / folder, candidate module location '${removeFileExtension(module.path)}', target file type 'TypeScript'.`); - getExpectedMissedLocationResolutionTrace(host, expectedTrace, getDirectoryPath(normalizePath(combinePaths(getDirectoryPath(file.path), moduleName))), module, moduleName.substring(moduleName.lastIndexOf("/") + 1), /*useNodeModules*/ false); - getExpectedResolutionTraceFooter(expectedTrace, module, moduleName, /*addRealPathTrace*/ false); - return expectedTrace; - } - - function getExpectedNonRelativeModuleResolutionTrace(host: TestServerHost, file: File, module: File, moduleName: string, expectedTrace: string[] = []) { - getExpectedResolutionTraceHeader(expectedTrace, file, moduleName); - expectedTrace.push(`Loading module '${moduleName}' from 'node_modules' folder, target file type 'TypeScript'.`); - getExpectedMissedLocationResolutionTrace(host, expectedTrace, getDirectoryPath(file.path), module, moduleName, /*useNodeModules*/ true); - getExpectedResolutionTraceFooter(expectedTrace, module, moduleName, /*addRealPathTrace*/ true); - return expectedTrace; - } - - function getExpectedNonRelativeModuleResolutionFromCacheTrace(host: TestServerHost, file: File, module: File, moduleName: string, cacheLocation: string, expectedTrace: string[] = []) { - getExpectedResolutionTraceHeader(expectedTrace, file, moduleName); - expectedTrace.push(`Loading module '${moduleName}' from 'node_modules' folder, target file type 'TypeScript'.`); - getExpectedMissedLocationResolutionTrace(host, expectedTrace, getDirectoryPath(file.path), module, moduleName, /*useNodeModules*/ true, cacheLocation); - expectedTrace.push(`Resolution for module '${moduleName}' was found in cache from location '${cacheLocation}'.`); - getExpectedResolutionTraceFooter(expectedTrace, module, moduleName, /*addRealPathTrace*/ false, /*ignoreModuleFileFound*/ true); - return expectedTrace; - } - - function getExpectedReusingResolutionFromOldProgram(file: File, moduleName: string) { - return `Reusing resolution of module '${moduleName}' to file '${file.path}' from old program.`; - } - - function verifyWatchesWithConfigFile(host: TestServerHost, files: File[], openFile: File, extraExpectedDirectories?: ReadonlyArray) { - const expectedRecursiveDirectories = arrayToSet([projectLocation, `${projectLocation}/${nodeModulesAtTypes}`, ...(extraExpectedDirectories || emptyArray)]); - checkWatchedFiles(host, mapDefined(files, f => { - if (f === openFile) { - return undefined; - } - const indexOfNodeModules = f.path.indexOf("/node_modules/"); - if (indexOfNodeModules === -1) { - return f.path; - } - expectedRecursiveDirectories.set(f.path.substr(0, indexOfNodeModules + "/node_modules".length), true); - return undefined; - })); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, arrayFrom(expectedRecursiveDirectories.keys()), /*recursive*/ true); - } - - describe("from files in same folder", () => { - function getFiles(fileContent: string) { - const file1: File = { - path: `${projectLocation}/src/file1.ts`, - content: fileContent - }; - const file2: File = { - path: `${projectLocation}/src/file2.ts`, - content: fileContent - }; - return { file1, file2 }; - } - - it("relative module name", () => { - const module1Name = "./module1"; - const module2Name = "../module2"; - const fileContent = `import { module1 } from "${module1Name}";import { module2 } from "${module2Name}";`; - const { file1, file2 } = getFiles(fileContent); - const { module1, module2 } = getModules(`${projectLocation}/src/module1.ts`, `${projectLocation}/module2.ts`); - const files = [module1, module2, file1, file2, configFile, libFile]; - const host = createServerHost(files); - const resolutionTrace = createHostModuleResolutionTrace(host); - const service = createProjectService(host); - service.openClientFile(file1.path); - const expectedTrace = getExpectedRelativeModuleResolutionTrace(host, file1, module1, module1Name); - getExpectedRelativeModuleResolutionTrace(host, file1, module2, module2Name, expectedTrace); - verifyTrace(resolutionTrace, expectedTrace); - verifyWatchesWithConfigFile(host, files, file1); - - file1.content += fileContent; - file2.content += fileContent; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - verifyTrace(resolutionTrace, [ - getExpectedReusingResolutionFromOldProgram(file1, module1Name), - getExpectedReusingResolutionFromOldProgram(file1, module2Name) - ]); - verifyWatchesWithConfigFile(host, files, file1); - }); - - it("non relative module name", () => { - const expectedNonRelativeDirectories = [`${projectLocation}/node_modules`, `${projectLocation}/src`]; - const module1Name = "module1"; - const module2Name = "module2"; - const fileContent = `import { module1 } from "${module1Name}";import { module2 } from "${module2Name}";`; - const { file1, file2 } = getFiles(fileContent); - const { module1, module2 } = getModules(`${projectLocation}/src/node_modules/module1/index.ts`, `${projectLocation}/node_modules/module2/index.ts`); - const files = [module1, module2, file1, file2, configFile, libFile]; - const host = createServerHost(files); - const resolutionTrace = createHostModuleResolutionTrace(host); - const service = createProjectService(host); - service.openClientFile(file1.path); - const expectedTrace = getExpectedNonRelativeModuleResolutionTrace(host, file1, module1, module1Name); - getExpectedNonRelativeModuleResolutionTrace(host, file1, module2, module2Name, expectedTrace); - verifyTrace(resolutionTrace, expectedTrace); - verifyWatchesWithConfigFile(host, files, file1, expectedNonRelativeDirectories); - - file1.content += fileContent; - file2.content += fileContent; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - verifyTrace(resolutionTrace, [ - getExpectedReusingResolutionFromOldProgram(file1, module1Name), - getExpectedReusingResolutionFromOldProgram(file1, module2Name) - ]); - verifyWatchesWithConfigFile(host, files, file1, expectedNonRelativeDirectories); - }); - }); - - describe("from files in different folders", () => { - function getFiles(fileContent1: string, fileContent2 = fileContent1, fileContent3 = fileContent1, fileContent4 = fileContent1) { - const file1: File = { - path: `${projectLocation}/product/src/file1.ts`, - content: fileContent1 - }; - const file2: File = { - path: `${projectLocation}/product/src/feature/file2.ts`, - content: fileContent2 - }; - const file3: File = { - path: `${projectLocation}/product/test/src/file3.ts`, - content: fileContent3 - }; - const file4: File = { - path: `${projectLocation}/product/test/file4.ts`, - content: fileContent4 - }; - return { file1, file2, file3, file4 }; - } - - it("relative module name", () => { - const module1Name = "./module1"; - const module2Name = "../module2"; - const module3Name = "../module1"; - const module4Name = "../../module2"; - const module5Name = "../../src/module1"; - const module6Name = "../src/module1"; - const fileContent1 = `import { module1 } from "${module1Name}";import { module2 } from "${module2Name}";`; - const fileContent2 = `import { module1 } from "${module3Name}";import { module2 } from "${module4Name}";`; - const fileContent3 = `import { module1 } from "${module5Name}";import { module2 } from "${module4Name}";`; - const fileContent4 = `import { module1 } from "${module6Name}";import { module2 } from "${module2Name}";`; - const { file1, file2, file3, file4 } = getFiles(fileContent1, fileContent2, fileContent3, fileContent4); - const { module1, module2 } = getModules(`${projectLocation}/product/src/module1.ts`, `${projectLocation}/product/module2.ts`); - const files = [module1, module2, file1, file2, file3, file4, configFile, libFile]; - const host = createServerHost(files); - const resolutionTrace = createHostModuleResolutionTrace(host); - const service = createProjectService(host); - service.openClientFile(file1.path); - const expectedTrace = getExpectedRelativeModuleResolutionTrace(host, file1, module1, module1Name); - getExpectedRelativeModuleResolutionTrace(host, file1, module2, module2Name, expectedTrace); - getExpectedRelativeModuleResolutionTrace(host, file2, module1, module3Name, expectedTrace); - getExpectedRelativeModuleResolutionTrace(host, file2, module2, module4Name, expectedTrace); - getExpectedRelativeModuleResolutionTrace(host, file4, module1, module6Name, expectedTrace); - getExpectedRelativeModuleResolutionTrace(host, file4, module2, module2Name, expectedTrace); - getExpectedRelativeModuleResolutionTrace(host, file3, module1, module5Name, expectedTrace); - getExpectedRelativeModuleResolutionTrace(host, file3, module2, module4Name, expectedTrace); - verifyTrace(resolutionTrace, expectedTrace); - verifyWatchesWithConfigFile(host, files, file1); - - file1.content += fileContent1; - file2.content += fileContent2; - file3.content += fileContent3; - file4.content += fileContent4; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - - verifyTrace(resolutionTrace, [ - getExpectedReusingResolutionFromOldProgram(file1, module1Name), - getExpectedReusingResolutionFromOldProgram(file1, module2Name) - ]); - verifyWatchesWithConfigFile(host, files, file1); - }); - - it("non relative module name", () => { - const expectedNonRelativeDirectories = [`${projectLocation}/node_modules`, `${projectLocation}/product`]; - const module1Name = "module1"; - const module2Name = "module2"; - const fileContent = `import { module1 } from "${module1Name}";import { module2 } from "${module2Name}";`; - const { file1, file2, file3, file4 } = getFiles(fileContent); - const { module1, module2 } = getModules(`${projectLocation}/product/node_modules/module1/index.ts`, `${projectLocation}/node_modules/module2/index.ts`); - const files = [module1, module2, file1, file2, file3, file4, configFile, libFile]; - const host = createServerHost(files); - const resolutionTrace = createHostModuleResolutionTrace(host); - const service = createProjectService(host); - service.openClientFile(file1.path); - const expectedTrace = getExpectedNonRelativeModuleResolutionTrace(host, file1, module1, module1Name); - getExpectedNonRelativeModuleResolutionTrace(host, file1, module2, module2Name, expectedTrace); - getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file2, module1, module1Name, getDirectoryPath(file1.path), expectedTrace); - getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file2, module2, module2Name, getDirectoryPath(file1.path), expectedTrace); - getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file4, module1, module1Name, `${projectLocation}/product`, expectedTrace); - getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file4, module2, module2Name, `${projectLocation}/product`, expectedTrace); - getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file3, module1, module1Name, getDirectoryPath(file4.path), expectedTrace); - getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file3, module2, module2Name, getDirectoryPath(file4.path), expectedTrace); - verifyTrace(resolutionTrace, expectedTrace); - verifyWatchesWithConfigFile(host, files, file1, expectedNonRelativeDirectories); - - file1.content += fileContent; - file2.content += fileContent; - file3.content += fileContent; - file4.content += fileContent; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - - verifyTrace(resolutionTrace, [ - getExpectedReusingResolutionFromOldProgram(file1, module1Name), - getExpectedReusingResolutionFromOldProgram(file1, module2Name) - ]); - verifyWatchesWithConfigFile(host, files, file1, expectedNonRelativeDirectories); - }); - - it("non relative module name from inferred project", () => { - const module1Name = "module1"; - const module2Name = "module2"; - const file2Name = "./feature/file2"; - const file3Name = "../test/src/file3"; - const file4Name = "../test/file4"; - const importModuleContent = `import { module1 } from "${module1Name}";import { module2 } from "${module2Name}";`; - const { file1, file2, file3, file4 } = getFiles(`import "${file2Name}"; import "${file4Name}"; import "${file3Name}"; ${importModuleContent}`, importModuleContent, importModuleContent, importModuleContent); - const { module1, module2 } = getModules(`${projectLocation}/product/node_modules/module1/index.ts`, `${projectLocation}/node_modules/module2/index.ts`); - const files = [module1, module2, file1, file2, file3, file4, libFile]; - const host = createServerHost(files); - const resolutionTrace = createHostModuleResolutionTrace(host); - const service = createProjectService(host); - service.setCompilerOptionsForInferredProjects({ traceResolution: true }); - service.openClientFile(file1.path); - const expectedTrace = getExpectedRelativeModuleResolutionTrace(host, file1, file2, file2Name); - getExpectedRelativeModuleResolutionTrace(host, file1, file4, file4Name, expectedTrace); - getExpectedRelativeModuleResolutionTrace(host, file1, file3, file3Name, expectedTrace); - getExpectedNonRelativeModuleResolutionTrace(host, file1, module1, module1Name, expectedTrace); - getExpectedNonRelativeModuleResolutionTrace(host, file1, module2, module2Name, expectedTrace); - getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file2, module1, module1Name, getDirectoryPath(file1.path), expectedTrace); - getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file2, module2, module2Name, getDirectoryPath(file1.path), expectedTrace); - getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file4, module1, module1Name, `${projectLocation}/product`, expectedTrace); - getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file4, module2, module2Name, `${projectLocation}/product`, expectedTrace); - getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file3, module1, module1Name, getDirectoryPath(file4.path), expectedTrace); - getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file3, module2, module2Name, getDirectoryPath(file4.path), expectedTrace); - verifyTrace(resolutionTrace, expectedTrace); - - const currentDirectory = getDirectoryPath(file1.path); - const watchedFiles = mapDefined(files, f => f === file1 || f.path.indexOf("/node_modules/") !== -1 ? undefined : f.path); - forEachAncestorDirectory(currentDirectory, d => { - watchedFiles.push(combinePaths(d, "tsconfig.json"), combinePaths(d, "jsconfig.json")); - }); - const watchedRecursiveDirectories = getTypeRootsFromLocation(currentDirectory).concat([ - `${currentDirectory}/node_modules`, `${currentDirectory}/feature`, `${projectLocation}/product/${nodeModules}`, - `${projectLocation}/${nodeModules}`, `${projectLocation}/product/test/${nodeModules}`, - `${projectLocation}/product/test/src/${nodeModules}` - ]); - checkWatches(); - - file1.content += importModuleContent; - file2.content += importModuleContent; - file3.content += importModuleContent; - file4.content += importModuleContent; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - - verifyTrace(resolutionTrace, [ - getExpectedReusingResolutionFromOldProgram(file1, file2Name), - getExpectedReusingResolutionFromOldProgram(file1, file4Name), - getExpectedReusingResolutionFromOldProgram(file1, file3Name), - getExpectedReusingResolutionFromOldProgram(file1, module1Name), - getExpectedReusingResolutionFromOldProgram(file1, module2Name) - ]); - checkWatches(); - - function checkWatches() { - checkWatchedFiles(host, watchedFiles); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); - } - }); - }); - - describe("when watching directories for failed lookup locations in amd resolution", () => { - const projectRoot = "/user/username/projects/project"; - const nodeFile: File = { - path: `${projectRoot}/src/typings/node.d.ts`, - content: ` -declare module "fs" { - export interface something { - } -}` - }; - const electronFile: File = { - path: `${projectRoot}/src/typings/electron.d.ts`, - content: ` -declare module 'original-fs' { - import * as fs from 'fs'; - export = fs; -}` - }; - const srcFile: File = { - path: `${projectRoot}/src/somefolder/srcfile.ts`, - content: ` -import { x } from "somefolder/module1"; -import { x } from "somefolder/module2"; -const y = x;` - }; - const moduleFile: File = { - path: `${projectRoot}/src/somefolder/module1.ts`, - content: ` -export const x = 10;` - }; - const configFile: File = { - path: `${projectRoot}/src/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - module: "amd", - moduleResolution: "classic", - target: "es5", - outDir: "../out", - baseUrl: "./", - typeRoots: ["typings"] - } - }) - }; - - function verifyModuleResolution(useNodeFile: boolean) { - const files = [...(useNodeFile ? [nodeFile] : []), electronFile, srcFile, moduleFile, configFile, libFile]; - const host = createServerHost(files); - const service = createProjectService(host); - service.openClientFile(srcFile.path, srcFile.content, ScriptKind.TS, projectRoot); - checkProjectActualFiles(service.configuredProjects.get(configFile.path)!, files.map(f => f.path)); - checkWatchedFilesDetailed(host, mapDefined(files, f => f === srcFile ? undefined : f.path), 1); - if (useNodeFile) { - checkWatchedDirectories(host, emptyArray, /*recursive*/ false); // since fs resolves to ambient module, shouldnt watch failed lookup - } - else { - checkWatchedDirectoriesDetailed(host, [`${projectRoot}`, `${projectRoot}/src`], 1, /*recursive*/ false); // failed lookup for fs - } - const expectedWatchedDirectories = createMap(); - expectedWatchedDirectories.set(`${projectRoot}/src`, 1); // Wild card - expectedWatchedDirectories.set(`${projectRoot}/src/somefolder`, 1); // failedLookup for somefolder/module2 - expectedWatchedDirectories.set(`${projectRoot}/src/node_modules`, 1); // failed lookup for somefolder/module2 - expectedWatchedDirectories.set(`${projectRoot}/somefolder`, 1); // failed lookup for somefolder/module2 - expectedWatchedDirectories.set(`${projectRoot}/node_modules`, 1); // failed lookup for with node_modules/@types/fs - expectedWatchedDirectories.set(`${projectRoot}/src/typings`, useNodeFile ? 1 : 2); // typeroot directory + failed lookup if not using node file - checkWatchedDirectoriesDetailed(host, expectedWatchedDirectories, /*recursive*/ true); - } - - it("when resolves to ambient module", () => { - verifyModuleResolution(/*useNodeFile*/ true); - }); - - it("when resolution fails", () => { - verifyModuleResolution(/*useNodeFile*/ false); - }); - }); - - describe("ignores files/folder changes in node_modules that start with '.'", () => { - const projectPath = "/user/username/projects/project"; - const npmCacheFile: File = { - path: `${projectPath}/node_modules/.cache/babel-loader/89c02171edab901b9926470ba6d5677e.ts`, - content: JSON.stringify({ something: 10 }) - }; - const file1: File = { - path: `${projectPath}/test.ts`, - content: `import { x } from "somemodule";` - }; - const file2: File = { - path: `${projectPath}/node_modules/somemodule/index.d.ts`, - content: `export const x = 10;` - }; - it("when watching node_modules in inferred project for failed lookup/closed script infos", () => { - const files = [libFile, file1, file2]; - const host = createServerHost(files); - const service = createProjectService(host); - service.openClientFile(file1.path); - checkNumberOfProjects(service, { inferredProjects: 1 }); - const project = service.inferredProjects[0]; - checkProjectActualFiles(project, files.map(f => f.path)); - (project as ResolutionCacheHost).maxNumberOfFilesToIterateForInvalidation = 1; - host.checkTimeoutQueueLength(0); - - host.ensureFileOrFolder(npmCacheFile); - host.checkTimeoutQueueLength(0); - }); - it("when watching node_modules as part of wild card directories in config project", () => { - const config: File = { - path: `${projectPath}/tsconfig.json`, - content: "{}" - }; - const files = [libFile, file1, file2, config]; - const host = createServerHost(files); - const service = createProjectService(host); - service.openClientFile(file1.path); - checkNumberOfProjects(service, { configuredProjects: 1 }); - const project = Debug.assertDefined(service.configuredProjects.get(config.path)); - checkProjectActualFiles(project, files.map(f => f.path)); - host.checkTimeoutQueueLength(0); - - host.ensureFileOrFolder(npmCacheFile); - host.checkTimeoutQueueLength(0); - }); - }); - }); - describe("tsserverProjectSystem getEditsForFileRename", () => { it("works for host implementing 'resolveModuleNames' and 'getResolvedModuleWithFailedLookupLocationsFromCache'", () => { const userTs: File = { diff --git a/src/testRunner/unittests/typingsInstaller.ts b/src/testRunner/unittests/typingsInstaller.ts index 34c5fa2a41..2b18af6a71 100644 --- a/src/testRunner/unittests/typingsInstaller.ts +++ b/src/testRunner/unittests/typingsInstaller.ts @@ -1,5 +1,4 @@ namespace ts.projectSystem { - import TI = server.typingsInstaller; import validatePackageName = JsTyping.validatePackageName; import PackageNameValidationResult = JsTyping.PackageNameValidationResult; diff --git a/src/testRunner/unittests/watchApi.ts b/src/testRunner/unittests/watchApi.ts new file mode 100644 index 0000000000..60072e2473 --- /dev/null +++ b/src/testRunner/unittests/watchApi.ts @@ -0,0 +1,40 @@ +namespace ts.tscWatch { + describe("watchAPI:: tsc-watch with custom module resolution", () => { + const projectRoot = "/user/username/projects/project"; + const configFileJson: any = { + compilerOptions: { module: "commonjs", resolveJsonModule: true }, + files: ["index.ts"] + }; + const mainFile: File = { + path: `${projectRoot}/index.ts`, + content: "import settings from './settings.json';" + }; + const config: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify(configFileJson) + }; + const settingsJson: File = { + path: `${projectRoot}/settings.json`, + content: JSON.stringify({ content: "Print this" }) + }; + + it("verify that module resolution with json extension works when returned without extension", () => { + const files = [libFile, mainFile, config, settingsJson]; + const host = createWatchedSystem(files, { currentDirectory: projectRoot }); + const compilerHost = createWatchCompilerHostOfConfigFile(config.path, {}, host); + const parsedCommandResult = parseJsonConfigFileContent(configFileJson, host, config.path); + compilerHost.resolveModuleNames = (moduleNames, containingFile) => moduleNames.map(m => { + const result = resolveModuleName(m, containingFile, parsedCommandResult.options, compilerHost); + const resolvedModule = result.resolvedModule!; + return { + resolvedFileName: resolvedModule.resolvedFileName, + isExternalLibraryImport: resolvedModule.isExternalLibraryImport, + originalFileName: resolvedModule.originalPath, + }; + }); + const watch = createWatchProgram(compilerHost); + const program = watch.getCurrentProgram().getProgram(); + checkProgramActualFiles(program, [mainFile.path, libFile.path, settingsJson.path]); + }); + }); +} diff --git a/src/testRunner/unittests/watchEnvironment.ts b/src/testRunner/unittests/watchEnvironment.ts index 5b620b54e8..74a6637d6f 100644 --- a/src/testRunner/unittests/watchEnvironment.ts +++ b/src/testRunner/unittests/watchEnvironment.ts @@ -262,5 +262,55 @@ namespace ts { verifyCompletionListWithNewFileInSubFolder(Tsc_WatchDirectory.DynamicPolling); }); }); + + describe("watchEnvironment:: tsserverProjectSystem Watched recursive directories with windows style file system", () => { + function verifyWatchedDirectories(rootedPath: string, useProjectAtRoot: boolean) { + const root = useProjectAtRoot ? rootedPath : `${rootedPath}myfolder/allproject/`; + const configFile: File = { + path: root + "project/tsconfig.json", + content: "{}" + }; + const file1: File = { + path: root + "project/file1.ts", + content: "let x = 10;" + }; + const file2: File = { + path: root + "project/file2.ts", + content: "let y = 10;" + }; + const files = [configFile, file1, file2, libFile]; + const host = createServerHost(files, { useWindowsStylePaths: true }); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + const project = projectService.configuredProjects.get(configFile.path)!; + assert.isDefined(project); + const winsowsStyleLibFilePath = "c:/" + libFile.path.substring(1); + checkProjectActualFiles(project, files.map(f => f === libFile ? winsowsStyleLibFilePath : f.path)); + checkWatchedFiles(host, mapDefined(files, f => f === libFile ? winsowsStyleLibFilePath : f === file1 ? undefined : f.path)); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, [ + root + "project", + root + "project/node_modules/@types" + ].concat(useProjectAtRoot ? [] : [root + nodeModulesAtTypes]), /*recursive*/ true); + } + + function verifyRootedDirectoryWatch(rootedPath: string) { + it("When project is in rootFolder of style c:/", () => { + verifyWatchedDirectories(rootedPath, /*useProjectAtRoot*/ true); + }); + + it("When files at some folder other than root", () => { + verifyWatchedDirectories(rootedPath, /*useProjectAtRoot*/ false); + }); + } + + describe("for rootFolder of style c:/", () => { + verifyRootedDirectoryWatch("c:/"); + }); + + describe("for rootFolder of style c:/users/username", () => { + verifyRootedDirectoryWatch("c:/users/username/"); + }); + }); } }