Split resolutionCache and watchApi tests into its own unittest
This commit is contained in:
parent
47200acfcd
commit
7b9b0f8da7
|
@ -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",
|
||||
|
|
981
src/testRunner/unittests/resolutionCache.ts
Normal file
981
src/testRunner/unittests/resolutionCache.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
namespace ts.tscWatch {
|
||||
export import libFile = TestFSWithWatch.libFile;
|
||||
import projectsLocation = TestFSWithWatch.tsbuildProjectsLocation;
|
||||
import getFilePathInProject = TestFSWithWatch.getTsBuildProjectFilePath;
|
||||
import getFileFromProject = TestFSWithWatch.getTsBuildProjectFile;
|
||||
|
|
237
src/testRunner/unittests/tscWatchHelpers.ts
Normal file
237
src/testRunner/unittests/tscWatchHelpers.ts
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
679
src/testRunner/unittests/tsserverHelpers.ts
Normal file
679
src/testRunner/unittests/tsserverHelpers.ts
Normal 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
|
@ -1,5 +1,4 @@
|
|||
namespace ts.projectSystem {
|
||||
import TI = server.typingsInstaller;
|
||||
import validatePackageName = JsTyping.validatePackageName;
|
||||
import PackageNameValidationResult = JsTyping.PackageNameValidationResult;
|
||||
|
||||
|
|
40
src/testRunner/unittests/watchApi.ts
Normal file
40
src/testRunner/unittests/watchApi.ts
Normal 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]);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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/");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue