Split resolutionCache and watchApi tests into its own unittest

This commit is contained in:
Sheetal Nandi 2018-12-07 10:30:10 -08:00
parent 47200acfcd
commit 7b9b0f8da7
10 changed files with 1993 additions and 1714 deletions

View file

@ -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",

View file

@ -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<string>) {
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<number>();
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);
});
});
});
}

View file

@ -1,5 +1,4 @@
namespace ts.tscWatch {
export import libFile = TestFSWithWatch.libFile;
import projectsLocation = TestFSWithWatch.tsbuildProjectsLocation;
import getFilePathInProject = TestFSWithWatch.getTsBuildProjectFilePath;
import getFileFromProject = TestFSWithWatch.getTsBuildProjectFile;

View file

@ -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<string>) {
checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles);
}
export function checkProgramRootFiles(program: Program, expectedFiles: ReadonlyArray<string>) {
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<true>();
// 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<Diagnostic> | ReadonlyArray<string>,
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<Diagnostic> | ReadonlyArray<string>) {
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<Diagnostic> | ReadonlyArray<string>, 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<Diagnostic> | ReadonlyArray<string>, 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<Diagnostic> | ReadonlyArray<string>, 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);
}
}

View file

@ -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<string>) {
checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles);
}
export function checkProgramRootFiles(program: Program, expectedFiles: ReadonlyArray<string>) {
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<Diagnostic> | ReadonlyArray<string>,
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<Diagnostic> | ReadonlyArray<string>) {
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<Diagnostic> | ReadonlyArray<string>, 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<Diagnostic> | ReadonlyArray<string>, 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<Diagnostic> | ReadonlyArray<string>, 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]);
});
});
}

View file

@ -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: <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<MapLike<string>>(),
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<string>) {
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<any> = {};
for (const typing of installedTypings) {
dependencies[typing] = "1.0.0";
}
return JSON.stringify({ dependencies });
}
export function createTypesRegistry(...list: string[]): Map<MapLike<string>> {
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<MapLike<string>>();
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>): 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<server.ProjectServiceEvent> {
const events = this.events;
this.events = [];
return events;
}
getEvent<T extends server.ProjectServiceEvent>(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<T extends server.ProjectServiceEvent>(eventName: T["eventName"]) {
this.events.forEach(event => assert.notEqual(event.eventName, eventName));
}
checkSingleConfigFileDiagEvent(configFileName: string, triggerFile: string, errors: ReadonlyArray<ConfigFileDiagnostic>) {
const eventData = this.getEvent<server.ConfigFileDiagEvent>(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<server.ProjectInfoTelemetryEventData>, configFile = "/tsconfig.json"): void {
assert.deepEqual<server.ProjectInfoTelemetryEventData>(this.getEvent<server.ProjectInfoTelemetryEvent>(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<server.OpenFileInfoTelemetryEventData>(this.getEvent<server.OpenFileInfoTelemetryEvent>(server.OpenFileInfoTelemetryEvent), { info });
}
assertNoOpenFilesTelemetryEvent(): void {
this.hasZeroEvent<server.OpenFileInfoTelemetryEvent>(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<T extends server.protocol.Request>(request: Partial<T>) {
this.seq++;
request.seq = this.seq;
request.type = "request";
return this.executeCommand(<T>request);
}
public event<T extends object>(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<server.SessionOptions> = {}) {
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<T extends server.ProjectServiceEvent>(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<T extends protocol.AnyEvent>(host: TestServerHost, eventNames: T["event"] | T["event"][], opts: Partial<server.SessionOptions> = {}) {
// 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<server.ProjectServiceOptions> = {}) {
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<server.ProjectServiceOptions>) {
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<string>) {
checkArray(`${server.ProjectKind[project.projectKind]} project, actual files`, project.getFileNames(), expectedFiles);
}
//function checkProjectRootFiles(project: server.Project, expectedFiles: ReadonlyArray<string>) {
// 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<T>(command: string, args: T): protocol.Request {
return {
seq: 0,
type: "request",
command,
arguments: args
};
}
export function executeSessionRequest<TRequest extends protocol.Request, TResponse extends protocol.Response>(session: server.Session, command: TRequest["command"], args: TRequest["arguments"]): TResponse["body"] {
return session.executeCommand(makeSessionRequest(command, args)).response as TResponse["body"];
}
export function executeSessionRequestNoResponse<TRequest extends protocol.Request>(session: server.Session, command: TRequest["command"], args: TRequest["arguments"]): void {
session.executeCommand(makeSessionRequest(command, args));
}
export function openFilesForSession(files: ReadonlyArray<File | { readonly file: File | string, readonly projectRootPath: string }>, session: server.Session): void {
for (const file of files) {
session.executeCommand(makeSessionRequest<protocol.OpenRequestArgs>(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<File>, session: server.Session): void {
for (const file of files) {
session.executeCommand(makeSessionRequest<protocol.FileRequestArgs>(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<string> = [], 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<File>): 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 })));
//}
}

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,4 @@
namespace ts.projectSystem {
import TI = server.typingsInstaller;
import validatePackageName = JsTyping.validatePackageName;
import PackageNameValidationResult = JsTyping.PackageNameValidationResult;

View file

@ -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]);
});
});
}

View file

@ -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/");
});
});
}
}