diff --git a/Jakefile.js b/Jakefile.js
index 6f940ca2ab3..86b79463bdb 100644
--- a/Jakefile.js
+++ b/Jakefile.js
@@ -209,6 +209,9 @@ var harnessSources = harnessCoreSources.concat([
"convertCompilerOptionsFromJson.ts",
"convertTypingOptionsFromJson.ts",
"tsserverProjectSystem.ts",
+ "compileOnSave.ts",
+ "typingsInstaller.ts",
+ "projectErrors.ts",
"matchFiles.ts",
"initializeTSConfig.ts",
].map(function (f) {
diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json
index a21faf9dd07..77a94cbd6a4 100644
--- a/src/harness/tsconfig.json
+++ b/src/harness/tsconfig.json
@@ -90,6 +90,9 @@
"./unittests/convertTypingOptionsFromJson.ts",
"./unittests/tsserverProjectSystem.ts",
"./unittests/matchFiles.ts",
- "./unittests/initializeTSConfig.ts"
+ "./unittests/initializeTSConfig.ts",
+ "./unittests/compileOnSave.ts",
+ "./unittests/typingsInstaller.ts",
+ "./unittests/projectErrors.ts"
]
}
diff --git a/src/harness/unittests/compileOnSave.ts b/src/harness/unittests/compileOnSave.ts
new file mode 100644
index 00000000000..f1895912230
--- /dev/null
+++ b/src/harness/unittests/compileOnSave.ts
@@ -0,0 +1,495 @@
+///
+///
+///
+
+namespace ts.projectSystem {
+ describe("CompileOnSave affected list", () => {
+ function sendAffectedFileRequestAndCheckResult(session: server.Session, request: server.protocol.Request, expectedFileList: { projectFileName: string, files: FileOrFolder[] }[]) {
+ const response: server.protocol.CompileOnSaveAffectedFileListSingleProject[] = session.executeCommand(request).response;
+ const actualResult = response.sort((list1, list2) => compareStrings(list1.projectFileName, list2.projectFileName));
+ expectedFileList = expectedFileList.sort((list1, list2) => compareStrings(list1.projectFileName, list2.projectFileName));
+
+ assert.equal(actualResult.length, expectedFileList.length, `Actual result project number is different from the expected project number`);
+
+ for (let i = 0; i < actualResult.length; i++) {
+ const actualResultSingleProject = actualResult[i];
+ const expectedResultSingleProject = expectedFileList[i];
+ assert.equal(actualResultSingleProject.projectFileName, expectedResultSingleProject.projectFileName, `Actual result contains different projects than the expected result`);
+
+ const actualResultSingleProjectFileNameList = actualResultSingleProject.fileNames.sort();
+ const expectedResultSingleProjectFileNameList = map(expectedResultSingleProject.files, f => f.path).sort();
+ assert.isTrue(
+ arrayIsEqualTo(actualResultSingleProjectFileNameList, expectedResultSingleProjectFileNameList),
+ `For project ${actualResultSingleProject.projectFileName}, the actual result is ${actualResultSingleProjectFileNameList}, while expected ${expectedResultSingleProjectFileNameList}`);
+ }
+ }
+
+ describe("for configured projects", () => {
+ let moduleFile1: FileOrFolder;
+ let file1Consumer1: FileOrFolder;
+ let file1Consumer2: FileOrFolder;
+ let moduleFile2: FileOrFolder;
+ let globalFile3: FileOrFolder;
+ let configFile: FileOrFolder;
+ let changeModuleFile1ShapeRequest1: server.protocol.Request;
+ let changeModuleFile1InternalRequest1: server.protocol.Request;
+ let changeModuleFile1ShapeRequest2: server.protocol.Request;
+ // A compile on save affected file request using file1
+ let moduleFile1FileListRequest: server.protocol.Request;
+
+ function createSession(host: server.ServerHost) {
+ const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
+ return new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
+ }
+
+ beforeEach(() => {
+ moduleFile1 = {
+ path: "/a/b/moduleFile1.ts",
+ content: "export function Foo() { };"
+ };
+
+ file1Consumer1 = {
+ path: "/a/b/file1Consumer1.ts",
+ content: `import {Foo} from "./moduleFile1"; export var y = 10;`
+ };
+
+ file1Consumer2 = {
+ path: "/a/b/file1Consumer2.ts",
+ content: `import {Foo} from "./moduleFile1"; let z = 10;`
+ };
+
+ moduleFile2 = {
+ path: "/a/b/moduleFile2.ts",
+ content: `export var Foo4 = 10;`
+ };
+
+ globalFile3 = {
+ path: "/a/b/globalFile3.ts",
+ content: `interface GlobalFoo { age: number }`
+ };
+
+ configFile = {
+ path: "/a/b/tsconfig.json",
+ content: `{
+ "compileOnSave": true
+ }`
+ };
+
+ // Change the content of file1 to `export var T: number;export function Foo() { };`
+ changeModuleFile1ShapeRequest1 = makeSessionRequest(server.CommandNames.Change, {
+ file: moduleFile1.path,
+ line: 1,
+ offset: 1,
+ endLine: 1,
+ endOffset: 1,
+ insertString: `export var T: number;`
+ });
+
+ // Change the content of file1 to `export var T: number;export function Foo() { };`
+ changeModuleFile1InternalRequest1 = makeSessionRequest(server.CommandNames.Change, {
+ file: moduleFile1.path,
+ line: 1,
+ offset: 1,
+ endLine: 1,
+ endOffset: 1,
+ insertString: `var T1: number;`
+ });
+
+ // Change the content of file1 to `export var T: number;export function Foo() { };`
+ changeModuleFile1ShapeRequest2 = makeSessionRequest(server.CommandNames.Change, {
+ file: moduleFile1.path,
+ line: 1,
+ offset: 1,
+ endLine: 1,
+ endOffset: 1,
+ insertString: `export var T2: number;`
+ });
+
+ moduleFile1FileListRequest = makeSessionRequest(server.CommandNames.CompileOnSaveAffectedFileList, { file: moduleFile1.path, projectFileName: configFile.path });
+ });
+
+ it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => {
+ const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]);
+ const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
+ const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
+
+ openFilesForSession([moduleFile1, file1Consumer1], session);
+
+ // Send an initial compileOnSave request
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]);
+ session.executeCommand(changeModuleFile1ShapeRequest1);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]);
+
+ // Change the content of file1 to `export var T: number;export function Foo() { console.log('hi'); };`
+ const changeFile1InternalRequest = makeSessionRequest(server.CommandNames.Change, {
+ file: moduleFile1.path,
+ line: 1,
+ offset: 46,
+ endLine: 1,
+ endOffset: 46,
+ insertString: `console.log('hi');`
+ });
+ session.executeCommand(changeFile1InternalRequest);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]);
+ });
+
+ it("should be up-to-date with the reference map changes", () => {
+ const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]);
+ const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
+ const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
+
+ openFilesForSession([moduleFile1, file1Consumer1], session);
+
+ // Send an initial compileOnSave request
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]);
+
+ // Change file2 content to `let y = Foo();`
+ const removeFile1Consumer1ImportRequest = makeSessionRequest(server.CommandNames.Change, {
+ file: file1Consumer1.path,
+ line: 1,
+ offset: 1,
+ endLine: 1,
+ endOffset: 28,
+ insertString: ""
+ });
+ session.executeCommand(removeFile1Consumer1ImportRequest);
+ session.executeCommand(changeModuleFile1ShapeRequest1);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer2] }]);
+
+ // Add the import statements back to file2
+ const addFile2ImportRequest = makeSessionRequest(server.CommandNames.Change, {
+ file: file1Consumer1.path,
+ line: 1,
+ offset: 1,
+ endLine: 1,
+ endOffset: 1,
+ insertString: `import {Foo} from "./moduleFile1";`
+ });
+ session.executeCommand(addFile2ImportRequest);
+
+ // Change the content of file1 to `export var T2: string;export var T: number;export function Foo() { };`
+ const changeModuleFile1ShapeRequest2 = makeSessionRequest(server.CommandNames.Change, {
+ file: moduleFile1.path,
+ line: 1,
+ offset: 1,
+ endLine: 1,
+ endOffset: 1,
+ insertString: `export var T2: string;`
+ });
+ session.executeCommand(changeModuleFile1ShapeRequest2);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]);
+ });
+
+ it("should be up-to-date with changes made in non-open files", () => {
+ const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]);
+ const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
+ const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
+
+ openFilesForSession([moduleFile1], session);
+
+ // Send an initial compileOnSave request
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]);
+
+ file1Consumer1.content = `let y = 10;`;
+ host.reloadFS([moduleFile1, file1Consumer1, file1Consumer2, configFile, libFile]);
+ host.triggerFileWatcherCallback(file1Consumer1.path, /*removed*/ false);
+
+ session.executeCommand(changeModuleFile1ShapeRequest1);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer2] }]);
+ });
+
+ it("should be up-to-date with deleted files", () => {
+ const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]);
+ const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
+ const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
+
+ openFilesForSession([moduleFile1], session);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]);
+
+ session.executeCommand(changeModuleFile1ShapeRequest1);
+ // Delete file1Consumer2
+ host.reloadFS([moduleFile1, file1Consumer1, configFile, libFile]);
+ host.triggerFileWatcherCallback(file1Consumer2.path, /*removed*/ true);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]);
+ });
+
+ it("should be up-to-date with newly created files", () => {
+ const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]);
+ const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
+ const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
+
+ openFilesForSession([moduleFile1], session);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]);
+
+ const file1Consumer3: FileOrFolder = {
+ path: "/a/b/file1Consumer3.ts",
+ content: `import {Foo} from "./moduleFile1"; let y = Foo();`
+ };
+ host.reloadFS([moduleFile1, file1Consumer1, file1Consumer2, file1Consumer3, globalFile3, configFile, libFile]);
+ host.triggerDirectoryWatcherCallback(ts.getDirectoryPath(file1Consumer3.path), file1Consumer3.path);
+ host.runQueuedTimeoutCallbacks();
+ session.executeCommand(changeModuleFile1ShapeRequest1);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2, file1Consumer3] }]);
+ });
+
+ it("should detect changes in non-root files", () => {
+ moduleFile1 = {
+ path: "/a/b/moduleFile1.ts",
+ content: "export function Foo() { };"
+ };
+
+ file1Consumer1 = {
+ path: "/a/b/file1Consumer1.ts",
+ content: `import {Foo} from "./moduleFile1"; let y = Foo();`
+ };
+
+ configFile = {
+ path: "/a/b/tsconfig.json",
+ content: `{
+ "compileOnSave": true,
+ "files": ["${file1Consumer1.path}"]
+ }`
+ };
+
+ const host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]);
+ const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
+ const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
+
+ openFilesForSession([moduleFile1, file1Consumer1], session);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]);
+
+ // change file1 shape now, and verify both files are affected
+ session.executeCommand(changeModuleFile1ShapeRequest1);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]);
+
+ // change file1 internal, and verify only file1 is affected
+ session.executeCommand(changeModuleFile1InternalRequest1);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]);
+ });
+
+ it("should return all files if a global file changed shape", () => {
+ const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]);
+ const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
+ const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
+
+ openFilesForSession([globalFile3], session);
+ const changeGlobalFile3ShapeRequest = makeSessionRequest(server.CommandNames.Change, {
+ file: globalFile3.path,
+ line: 1,
+ offset: 1,
+ endLine: 1,
+ endOffset: 1,
+ insertString: `var T2: string;`
+ });
+
+ // check after file1 shape changes
+ session.executeCommand(changeGlobalFile3ShapeRequest);
+ const globalFile3FileListRequest = makeSessionRequest(server.CommandNames.CompileOnSaveAffectedFileList, { file: globalFile3.path });
+ sendAffectedFileRequestAndCheckResult(session, globalFile3FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2] }]);
+ });
+
+ it("should return empty array if CompileOnSave is not enabled", () => {
+ configFile = {
+ path: "/a/b/tsconfig.json",
+ content: `{}`
+ };
+
+ const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile, libFile]);
+ const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
+ const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
+ openFilesForSession([moduleFile1], session);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, []);
+ });
+
+ it("should always return the file itself if '--isolatedModules' is specified", () => {
+ configFile = {
+ path: "/a/b/tsconfig.json",
+ content: `{
+ "compileOnSave": true,
+ "compilerOptions": {
+ "isolatedModules": true
+ }
+ }`
+ };
+
+ const host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]);
+ const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
+ const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
+ openFilesForSession([moduleFile1], session);
+
+ const file1ChangeShapeRequest = makeSessionRequest(server.CommandNames.Change, {
+ file: moduleFile1.path,
+ line: 1,
+ offset: 27,
+ endLine: 1,
+ endOffset: 27,
+ insertString: `Point,`
+ });
+ session.executeCommand(file1ChangeShapeRequest);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]);
+ });
+
+ it("should always return the file itself if '--out' or '--outFile' is specified", () => {
+ configFile = {
+ path: "/a/b/tsconfig.json",
+ content: `{
+ "compileOnSave": true,
+ "compilerOptions": {
+ "module": "system",
+ "outFile": "/a/b/out.js"
+ }
+ }`
+ };
+
+ const host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]);
+ const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
+ const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
+ openFilesForSession([moduleFile1], session);
+
+ const file1ChangeShapeRequest = makeSessionRequest(server.CommandNames.Change, {
+ file: moduleFile1.path,
+ line: 1,
+ offset: 27,
+ endLine: 1,
+ endOffset: 27,
+ insertString: `Point,`
+ });
+ session.executeCommand(file1ChangeShapeRequest);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]);
+ });
+
+ it("should return cascaded affected file list", () => {
+ const file1Consumer1Consumer1: FileOrFolder = {
+ path: "/a/b/file1Consumer1Consumer1.ts",
+ content: `import {y} from "./file1Consumer1";`
+ };
+ const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer1Consumer1, globalFile3, configFile, libFile]);
+ const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
+ const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
+
+ openFilesForSession([moduleFile1, file1Consumer1], session);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer1Consumer1] }]);
+
+ const changeFile1Consumer1ShapeRequest = makeSessionRequest(server.CommandNames.Change, {
+ file: file1Consumer1.path,
+ line: 2,
+ offset: 1,
+ endLine: 2,
+ endOffset: 1,
+ insertString: `export var T: number;`
+ });
+ session.executeCommand(changeModuleFile1ShapeRequest1);
+ session.executeCommand(changeFile1Consumer1ShapeRequest);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer1Consumer1] }]);
+ });
+
+ it("should work fine for files with circular references", () => {
+ const file1: FileOrFolder = {
+ path: "/a/b/file1.ts",
+ content: `
+ ///
+ export var t1 = 10;`
+ };
+ const file2: FileOrFolder = {
+ path: "/a/b/file2.ts",
+ content: `
+ ///
+ export var t2 = 10;`
+ };
+ const host = createServerHost([file1, file2, configFile]);
+ const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
+ const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
+
+ openFilesForSession([file1, file2], session);
+ const file1AffectedListRequest = makeSessionRequest(server.CommandNames.CompileOnSaveAffectedFileList, { file: file1.path });
+ sendAffectedFileRequestAndCheckResult(session, file1AffectedListRequest, [{ projectFileName: configFile.path, files: [file1, file2] }]);
+ });
+
+ it("should return results for all projects if not specifying projectFileName", () => {
+ const file1: FileOrFolder = { path: "/a/b/file1.ts", content: "export var t = 10;" };
+ const file2: FileOrFolder = { path: "/a/b/file2.ts", content: `import {t} from "./file1"; var t2 = 11;` };
+ const file3: FileOrFolder = { path: "/a/c/file2.ts", content: `import {t} from "../b/file1"; var t3 = 11;` };
+ const configFile1: FileOrFolder = { path: "/a/b/tsconfig.json", content: `{ "compileOnSave": true }` };
+ const configFile2: FileOrFolder = { path: "/a/c/tsconfig.json", content: `{ "compileOnSave": true }` };
+
+ const host = createServerHost([file1, file2, file3, configFile1, configFile2]);
+ const session = createSession(host);
+
+ openFilesForSession([file1, file2, file3], session);
+ const file1AffectedListRequest = makeSessionRequest(server.CommandNames.CompileOnSaveAffectedFileList, { file: file1.path });
+
+ sendAffectedFileRequestAndCheckResult(session, file1AffectedListRequest, [
+ { projectFileName: configFile1.path, files: [file1, file2] },
+ { projectFileName: configFile2.path, files: [file1, file3] }
+ ]);
+ });
+
+ it("should detect removed code file", () => {
+ const referenceFile1: FileOrFolder = {
+ path: "/a/b/referenceFile1.ts",
+ content: `
+ ///
+ export var x = Foo();`
+ };
+ const host = createServerHost([moduleFile1, referenceFile1, configFile]);
+ const session = createSession(host);
+
+ openFilesForSession([referenceFile1], session);
+ host.reloadFS([referenceFile1, configFile]);
+ host.triggerFileWatcherCallback(moduleFile1.path, /*removed*/ true);
+
+ const request = makeSessionRequest(server.CommandNames.CompileOnSaveAffectedFileList, { file: referenceFile1.path });
+ sendAffectedFileRequestAndCheckResult(session, request, [
+ { projectFileName: configFile.path, files: [referenceFile1] }
+ ]);
+ const requestForMissingFile = makeSessionRequest(server.CommandNames.CompileOnSaveAffectedFileList, { file: moduleFile1.path });
+ sendAffectedFileRequestAndCheckResult(session, requestForMissingFile, []);
+ });
+
+ it("should detect non-existing code file", () => {
+ const referenceFile1: FileOrFolder = {
+ path: "/a/b/referenceFile1.ts",
+ content: `
+ ///
+ export var x = Foo();`
+ };
+ const host = createServerHost([referenceFile1, configFile]);
+ const session = createSession(host);
+
+ openFilesForSession([referenceFile1], session);
+ const request = makeSessionRequest(server.CommandNames.CompileOnSaveAffectedFileList, { file: referenceFile1.path });
+ sendAffectedFileRequestAndCheckResult(session, request, [
+ { projectFileName: configFile.path, files: [referenceFile1] }
+ ]);
+ });
+ });
+ });
+
+ describe("EmitFile test", () => {
+ it("should emit specified file", () => {
+ const file1 = {
+ path: "/a/b/f1.ts",
+ content: `export function Foo() { return 10; }`
+ };
+ const file2 = {
+ path: "/a/b/f2.ts",
+ content: `import {Foo} from "./f1"; let y = Foo();`
+ };
+ const configFile = {
+ path: "/a/b/tsconfig.json",
+ content: `{}`
+ };
+ const host = createServerHost([file1, file2, configFile, libFile]);
+ const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
+ const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
+
+ openFilesForSession([file1, file2], session);
+ const compileFileRequest = makeSessionRequest(server.CommandNames.CompileOnSaveEmitFile, { file: file1.path, projectFileName: configFile.path });
+ session.executeCommand(compileFileRequest);
+
+ const expectedEmittedFileName = "/a/b/f1.js";
+ assert.isTrue(host.fileExists(expectedEmittedFileName));
+ assert.equal(host.readFile(expectedEmittedFileName), `"use strict";\r\nfunction Foo() { return 10; }\r\nexports.Foo = Foo;\r\n`);
+ });
+ });
+}
\ No newline at end of file
diff --git a/src/harness/unittests/projectErrors.ts b/src/harness/unittests/projectErrors.ts
new file mode 100644
index 00000000000..3db3675dbd6
--- /dev/null
+++ b/src/harness/unittests/projectErrors.ts
@@ -0,0 +1,186 @@
+///
+///
+///
+
+namespace ts.projectSystem {
+ describe("Project errors", () => {
+ function checkProjectErrors(projectFiles: server.ProjectFilesWithTSDiagnostics, expectedErrors: string[]) {
+ assert.isTrue(projectFiles !== undefined, "missing project files");
+ const errors = projectFiles.projectErrors;
+ assert.equal(errors ? errors.length : 0, expectedErrors.length, `expected ${expectedErrors.length} error in the list`);
+ if (expectedErrors.length) {
+ for (let i = 0; i < errors.length; i++) {
+ const actualMessage = flattenDiagnosticMessageText(errors[i].messageText, "\n");
+ const expectedMessage = expectedErrors[i];
+ assert.isTrue(actualMessage.indexOf(expectedMessage) === 0, `error message does not match, expected ${actualMessage} to start with ${expectedMessage}`);
+ }
+ }
+ }
+
+ it("external project - diagnostics for missing files", () => {
+ const file1 = {
+ path: "/a/b/app.ts",
+ content: ""
+ };
+ const file2 = {
+ path: "/a/b/lib.ts",
+ content: ""
+ };
+ // only file1 exists - expect error
+ const host = createServerHost([file1]);
+ const projectService = createProjectService(host);
+ const projectFileName = "/a/b/test.csproj";
+
+ {
+ projectService.openExternalProject({
+ projectFileName,
+ options: {},
+ rootFiles: toExternalFiles([file1.path, file2.path])
+ });
+
+ projectService.checkNumberOfProjects({ externalProjects: 1 });
+ const knownProjects = projectService.synchronizeProjectList([]);
+ checkProjectErrors(knownProjects[0], ["File '/a/b/lib.ts' not found."]);
+ }
+ // only file2 exists - expect error
+ host.reloadFS([file2]);
+ {
+ projectService.openExternalProject({
+ projectFileName,
+ options: {},
+ rootFiles: toExternalFiles([file1.path, file2.path])
+ });
+ projectService.checkNumberOfProjects({ externalProjects: 1 });
+ const knownProjects = projectService.synchronizeProjectList([]);
+ checkProjectErrors(knownProjects[0], ["File '/a/b/app.ts' not found."]);
+ }
+
+ // both files exist - expect no errors
+ host.reloadFS([file1, file2]);
+ {
+ projectService.openExternalProject({
+ projectFileName,
+ options: {},
+ rootFiles: toExternalFiles([file1.path, file2.path])
+ });
+
+ projectService.checkNumberOfProjects({ externalProjects: 1 });
+ const knownProjects = projectService.synchronizeProjectList([]);
+ checkProjectErrors(knownProjects[0], []);
+ }
+ });
+
+ it("configured projects - diagnostics for missing files", () => {
+ const file1 = {
+ path: "/a/b/app.ts",
+ content: ""
+ };
+ const file2 = {
+ path: "/a/b/lib.ts",
+ content: ""
+ };
+ const config = {
+ path: "/a/b/tsconfig.json",
+ content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) })
+ };
+ const host = createServerHost([file1, config]);
+ const projectService = createProjectService(host);
+
+ projectService.openClientFile(file1.path);
+ projectService.checkNumberOfProjects({ configuredProjects: 1 });
+ checkProjectErrors(projectService.synchronizeProjectList([])[0], ["File '/a/b/lib.ts' not found."]);
+
+ host.reloadFS([file1, file2, config]);
+
+ projectService.openClientFile(file1.path);
+ projectService.checkNumberOfProjects({ configuredProjects: 1 });
+ checkProjectErrors(projectService.synchronizeProjectList([])[0], []);
+ });
+
+ it("configured projects - diagnostics for corrupted config 1", () => {
+ const file1 = {
+ path: "/a/b/app.ts",
+ content: ""
+ };
+ const file2 = {
+ path: "/a/b/lib.ts",
+ content: ""
+ };
+ const correctConfig = {
+ path: "/a/b/tsconfig.json",
+ content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) })
+ };
+ const corruptedConfig = {
+ path: correctConfig.path,
+ content: correctConfig.content.substr(1)
+ };
+ const host = createServerHost([file1, file2, corruptedConfig]);
+ const projectService = createProjectService(host);
+
+ projectService.openClientFile(file1.path);
+ {
+ projectService.checkNumberOfProjects({ configuredProjects: 1 });
+ const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f);
+ assert.isTrue(configuredProject !== undefined, "should find configured project");
+ checkProjectErrors(configuredProject, [
+ "')' expected.",
+ "Declaration or statement expected.",
+ "Declaration or statement expected.",
+ "Failed to parse file '/a/b/tsconfig.json'"
+ ]);
+ }
+ // fix config and trigger watcher
+ host.reloadFS([file1, file2, correctConfig]);
+ host.triggerFileWatcherCallback(correctConfig.path, /*false*/);
+ {
+ projectService.checkNumberOfProjects({ configuredProjects: 1 });
+ const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f);
+ assert.isTrue(configuredProject !== undefined, "should find configured project");
+ checkProjectErrors(configuredProject, []);
+ }
+ });
+
+ it("configured projects - diagnostics for corrupted config 2", () => {
+ const file1 = {
+ path: "/a/b/app.ts",
+ content: ""
+ };
+ const file2 = {
+ path: "/a/b/lib.ts",
+ content: ""
+ };
+ const correctConfig = {
+ path: "/a/b/tsconfig.json",
+ content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) })
+ };
+ const corruptedConfig = {
+ path: correctConfig.path,
+ content: correctConfig.content.substr(1)
+ };
+ const host = createServerHost([file1, file2, correctConfig]);
+ const projectService = createProjectService(host);
+
+ projectService.openClientFile(file1.path);
+ {
+ projectService.checkNumberOfProjects({ configuredProjects: 1 });
+ const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f);
+ assert.isTrue(configuredProject !== undefined, "should find configured project");
+ checkProjectErrors(configuredProject, []);
+ }
+ // break config and trigger watcher
+ host.reloadFS([file1, file2, corruptedConfig]);
+ host.triggerFileWatcherCallback(corruptedConfig.path, /*false*/);
+ {
+ projectService.checkNumberOfProjects({ configuredProjects: 1 });
+ const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f);
+ assert.isTrue(configuredProject !== undefined, "should find configured project");
+ checkProjectErrors(configuredProject, [
+ "')' expected.",
+ "Declaration or statement expected.",
+ "Declaration or statement expected.",
+ "Failed to parse file '/a/b/tsconfig.json'"
+ ]);
+ }
+ });
+ });
+}
\ No newline at end of file
diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts
index 12318ac2a7e..941215fc3ae 100644
--- a/src/harness/unittests/tsserverProjectSystem.ts
+++ b/src/harness/unittests/tsserverProjectSystem.ts
@@ -1,12 +1,12 @@
///
///
-namespace ts {
- function notImplemented(): any {
+namespace ts.projectSystem {
+ export function notImplemented(): any {
throw new Error("Not yet implemented");
}
- const nullLogger: server.Logger = {
+ export const nullLogger: server.Logger = {
close: () => void 0,
hasLevel: () => void 0,
loggingEnabled: () => false,
@@ -18,17 +18,17 @@ namespace ts {
getLogFileName: (): string => undefined
};
- const nullCancellationToken: HostCancellationToken = {
+ export const nullCancellationToken: HostCancellationToken = {
isCancellationRequested: () => false
};
- const { content: libFileContent } = Harness.getDefaultLibraryFile(Harness.IO);
- const libFile: FileOrFolder = {
+ export const { content: libFileContent } = Harness.getDefaultLibraryFile(Harness.IO);
+ export const libFile: FileOrFolder = {
path: "/a/lib/lib.d.ts",
content: libFileContent
};
- class TestTypingsInstaller extends server.typingsInstaller.TypingsInstaller implements server.ITypingsInstaller {
+ export class TestTypingsInstaller extends server.typingsInstaller.TypingsInstaller implements server.ITypingsInstaller {
protected projectService: server.ProjectService;
constructor(readonly globalTypingsCacheLocation: string, readonly installTypingHost: server.ServerHost) {
super(globalTypingsCacheLocation, "");
@@ -80,26 +80,26 @@ namespace ts {
}
}
- function getExecutingFilePathFromLibFile(libFilePath: string): string {
+ export function getExecutingFilePathFromLibFile(libFilePath: string): string {
return combinePaths(getDirectoryPath(libFile.path), "tsc.js");
}
- function toExternalFile(fileName: string): server.protocol.ExternalFile {
+ export function toExternalFile(fileName: string): server.protocol.ExternalFile {
return { fileName };
}
- function toExternalFiles(fileNames: string[]) {
+ export function toExternalFiles(fileNames: string[]) {
return map(fileNames, toExternalFile);
}
- interface TestServerHostCreationParameters {
+ export interface TestServerHostCreationParameters {
useCaseSensitiveFileNames?: boolean;
executingFilePath?: string;
libFile?: FileOrFolder;
currentDirectory?: string;
}
- function createServerHost(fileOrFolderList: FileOrFolder[],
+ export function createServerHost(fileOrFolderList: FileOrFolder[],
params?: TestServerHostCreationParameters,
libFilePath: string = libFile.path): TestServerHost {
@@ -113,7 +113,7 @@ namespace ts {
fileOrFolderList);
}
- interface CreateProjectServiceParameters {
+ export interface CreateProjectServiceParameters {
cancellationToken?: HostCancellationToken;
logger?: server.Logger;
useSingleInferredProject?: boolean;
@@ -122,7 +122,7 @@ namespace ts {
}
- class TestProjectService extends server.ProjectService {
+ export class TestProjectService extends server.ProjectService {
constructor(host: server.ServerHost, logger: server.Logger, cancellationToken: HostCancellationToken, useSingleInferredProject: boolean,
typingsInstaller: server.ITypingsInstaller, eventHandler: server.ProjectServiceEventHandler) {
super(host, logger, cancellationToken, useSingleInferredProject, typingsInstaller, eventHandler);
@@ -132,42 +132,42 @@ namespace ts {
checkNumberOfProjects(this, count);
}
}
- function createProjectService(host: server.ServerHost, parameters: CreateProjectServiceParameters = {}) {
+ export function createProjectService(host: server.ServerHost, parameters: CreateProjectServiceParameters = {}) {
const cancellationToken = parameters.cancellationToken || nullCancellationToken;
const logger = parameters.logger || nullLogger;
const useSingleInferredProject = parameters.useSingleInferredProject !== undefined ? parameters.useSingleInferredProject : false;
return new TestProjectService(host, logger, cancellationToken, useSingleInferredProject, parameters.typingsInstaller, parameters.eventHandler);
}
- interface FileOrFolder {
+ export interface FileOrFolder {
path: string;
content?: string;
fileSize?: number;
}
- interface FSEntry {
+ export interface FSEntry {
path: Path;
fullPath: string;
}
- interface File extends FSEntry {
+ export interface File extends FSEntry {
content: string;
fileSize?: number;
}
- interface Folder extends FSEntry {
+ export interface Folder extends FSEntry {
entries: FSEntry[];
}
- function isFolder(s: FSEntry): s is Folder {
+ export function isFolder(s: FSEntry): s is Folder {
return isArray((s).entries);
}
- function isFile(s: FSEntry): s is File {
+ export function isFile(s: FSEntry): s is File {
return typeof (s).content === "string";
}
- function addFolder(fullPath: string, toPath: (s: string) => Path, fs: FileMap): Folder {
+ export function addFolder(fullPath: string, toPath: (s: string) => Path, fs: FileMap): Folder {
const path = toPath(fullPath);
if (fs.contains(path)) {
Debug.assert(isFolder(fs.get(path)));
@@ -185,55 +185,55 @@ namespace ts {
return entry;
}
- function checkMapKeys(caption: string, map: Map, expectedKeys: string[]) {
+ export function checkMapKeys(caption: string, map: Map, expectedKeys: string[]) {
assert.equal(reduceProperties(map, count => count + 1, 0), expectedKeys.length, `${caption}: incorrect size of map`);
for (const name of expectedKeys) {
assert.isTrue(name in map, `${caption} is expected to contain ${name}, actual keys: ${Object.keys(map)}`);
}
}
- function checkFileNames(caption: string, actualFileNames: string[], expectedFileNames: string[]) {
+ export function checkFileNames(caption: string, actualFileNames: string[], expectedFileNames: string[]) {
assert.equal(actualFileNames.length, expectedFileNames.length, `${caption}: incorrect actual number of files, expected ${JSON.stringify(expectedFileNames)}, got ${actualFileNames}`);
for (const f of expectedFileNames) {
assert.isTrue(contains(actualFileNames, f), `${caption}: expected to find ${f} in ${JSON.stringify(actualFileNames)}`);
}
}
- function checkNumberOfConfiguredProjects(projectService: server.ProjectService, expected: number) {
+ export function checkNumberOfConfiguredProjects(projectService: server.ProjectService, expected: number) {
assert.equal(projectService.configuredProjects.length, expected, `expected ${expected} configured project(s)`);
}
- function checkNumberOfExternalProjects(projectService: server.ProjectService, expected: number) {
+ export function checkNumberOfExternalProjects(projectService: server.ProjectService, expected: number) {
assert.equal(projectService.externalProjects.length, expected, `expected ${expected} external project(s)`);
}
- function checkNumberOfInferredProjects(projectService: server.ProjectService, expected: number) {
+ export function checkNumberOfInferredProjects(projectService: server.ProjectService, expected: number) {
assert.equal(projectService.inferredProjects.length, expected, `expected ${expected} inferred project(s)`);
}
- function checkNumberOfProjects(projectService: server.ProjectService, count: { inferredProjects?: number, configuredProjects?: number, externalProjects?: number }) {
+ 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);
}
- function checkWatchedFiles(host: TestServerHost, expectedFiles: string[]) {
+ export function checkWatchedFiles(host: TestServerHost, expectedFiles: string[]) {
checkMapKeys("watchedFiles", host.watchedFiles, expectedFiles);
}
- function checkWatchedDirectories(host: TestServerHost, expectedDirectories: string[]) {
+ export function checkWatchedDirectories(host: TestServerHost, expectedDirectories: string[]) {
checkMapKeys("watchedDirectories", host.watchedDirectories, expectedDirectories);
}
- function checkProjectActualFiles(project: server.Project, expectedFiles: string[]) {
+ export function checkProjectActualFiles(project: server.Project, expectedFiles: string[]) {
checkFileNames(`${server.ProjectKind[project.projectKind]} project, actual files`, project.getFileNames(), expectedFiles);
}
- function checkProjectRootFiles(project: server.Project, expectedFiles: string[]) {
+ export function checkProjectRootFiles(project: server.Project, expectedFiles: string[]) {
checkFileNames(`${server.ProjectKind[project.projectKind]} project, rootFileNames`, project.getRootFiles(), expectedFiles);
}
- class Callbacks {
+ export class Callbacks {
private map: { [n: number]: TimeOutCallback } = {};
private nextId = 1;
@@ -269,9 +269,9 @@ namespace ts {
}
}
- type TimeOutCallback = () => any;
+ export type TimeOutCallback = () => any;
- class TestServerHost implements server.ServerHost {
+ export class TestServerHost implements server.ServerHost {
args: string[] = [];
newLine: "\n";
@@ -479,7 +479,7 @@ namespace ts {
readonly exit = () => notImplemented();
}
- function makeSessionRequest(command: string, args: T) {
+ export function makeSessionRequest(command: string, args: T) {
const newRequest: server.protocol.Request = {
seq: 0,
type: "request",
@@ -489,7 +489,7 @@ namespace ts {
return newRequest;
}
- function openFilesForSession(files: FileOrFolder[], session: server.Session) {
+ export function openFilesForSession(files: FileOrFolder[], session: server.Session) {
for (const file of files) {
const request = makeSessionRequest(server.CommandNames.Open, { file: file.path });
session.executeCommand(request);
@@ -1542,819 +1542,6 @@ namespace ts {
});
});
- describe("CompileOnSave affected list", () => {
- function sendAffectedFileRequestAndCheckResult(session: server.Session, request: server.protocol.Request, expectedFileList: { projectFileName: string, files: FileOrFolder[] }[]) {
- const response: server.protocol.CompileOnSaveAffectedFileListSingleProject[] = session.executeCommand(request).response;
- const actualResult = response.sort((list1, list2) => compareStrings(list1.projectFileName, list2.projectFileName));
- expectedFileList = expectedFileList.sort((list1, list2) => compareStrings(list1.projectFileName, list2.projectFileName));
-
- assert.equal(actualResult.length, expectedFileList.length, `Actual result project number is different from the expected project number`);
-
- for (let i = 0; i < actualResult.length; i++) {
- const actualResultSingleProject = actualResult[i];
- const expectedResultSingleProject = expectedFileList[i];
- assert.equal(actualResultSingleProject.projectFileName, expectedResultSingleProject.projectFileName, `Actual result contains different projects than the expected result`);
-
- const actualResultSingleProjectFileNameList = actualResultSingleProject.fileNames.sort();
- const expectedResultSingleProjectFileNameList = map(expectedResultSingleProject.files, f => f.path).sort();
- assert.isTrue(
- arrayIsEqualTo(actualResultSingleProjectFileNameList, expectedResultSingleProjectFileNameList),
- `For project ${actualResultSingleProject.projectFileName}, the actual result is ${actualResultSingleProjectFileNameList}, while expected ${expectedResultSingleProjectFileNameList}`);
- }
- }
-
- describe("for configured projects", () => {
- let moduleFile1: FileOrFolder;
- let file1Consumer1: FileOrFolder;
- let file1Consumer2: FileOrFolder;
- let moduleFile2: FileOrFolder;
- let globalFile3: FileOrFolder;
- let configFile: FileOrFolder;
- let changeModuleFile1ShapeRequest1: server.protocol.Request;
- let changeModuleFile1InternalRequest1: server.protocol.Request;
- let changeModuleFile1ShapeRequest2: server.protocol.Request;
- // A compile on save affected file request using file1
- let moduleFile1FileListRequest: server.protocol.Request;
-
- beforeEach(() => {
- moduleFile1 = {
- path: "/a/b/moduleFile1.ts",
- content: "export function Foo() { };"
- };
-
- file1Consumer1 = {
- path: "/a/b/file1Consumer1.ts",
- content: `import {Foo} from "./moduleFile1"; export var y = 10;`
- };
-
- file1Consumer2 = {
- path: "/a/b/file1Consumer2.ts",
- content: `import {Foo} from "./moduleFile1"; let z = 10;`
- };
-
- moduleFile2 = {
- path: "/a/b/moduleFile2.ts",
- content: `export var Foo4 = 10;`
- };
-
- globalFile3 = {
- path: "/a/b/globalFile3.ts",
- content: `interface GlobalFoo { age: number }`
- };
-
- configFile = {
- path: "/a/b/tsconfig.json",
- content: `{
- "compileOnSave": true
- }`
- };
-
- // Change the content of file1 to `export var T: number;export function Foo() { };`
- changeModuleFile1ShapeRequest1 = makeSessionRequest(server.CommandNames.Change, {
- file: moduleFile1.path,
- line: 1,
- offset: 1,
- endLine: 1,
- endOffset: 1,
- insertString: `export var T: number;`
- });
-
- // Change the content of file1 to `export var T: number;export function Foo() { };`
- changeModuleFile1InternalRequest1 = makeSessionRequest(server.CommandNames.Change, {
- file: moduleFile1.path,
- line: 1,
- offset: 1,
- endLine: 1,
- endOffset: 1,
- insertString: `var T1: number;`
- });
-
- // Change the content of file1 to `export var T: number;export function Foo() { };`
- changeModuleFile1ShapeRequest2 = makeSessionRequest(server.CommandNames.Change, {
- file: moduleFile1.path,
- line: 1,
- offset: 1,
- endLine: 1,
- endOffset: 1,
- insertString: `export var T2: number;`
- });
-
- moduleFile1FileListRequest = makeSessionRequest(server.CommandNames.CompileOnSaveAffectedFileList, { file: moduleFile1.path, projectFileName: configFile.path });
- });
-
- it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => {
- const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]);
- const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
- const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
-
- openFilesForSession([moduleFile1, file1Consumer1], session);
-
- // Send an initial compileOnSave request
- sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]);
- session.executeCommand(changeModuleFile1ShapeRequest1);
- sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]);
-
- // Change the content of file1 to `export var T: number;export function Foo() { console.log('hi'); };`
- const changeFile1InternalRequest = makeSessionRequest(server.CommandNames.Change, {
- file: moduleFile1.path,
- line: 1,
- offset: 46,
- endLine: 1,
- endOffset: 46,
- insertString: `console.log('hi');`
- });
- session.executeCommand(changeFile1InternalRequest);
- sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]);
- });
-
- it("should be up-to-date with the reference map changes", () => {
- const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]);
- const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
- const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
-
- openFilesForSession([moduleFile1, file1Consumer1], session);
-
- // Send an initial compileOnSave request
- sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]);
-
- // Change file2 content to `let y = Foo();`
- const removeFile1Consumer1ImportRequest = makeSessionRequest(server.CommandNames.Change, {
- file: file1Consumer1.path,
- line: 1,
- offset: 1,
- endLine: 1,
- endOffset: 28,
- insertString: ""
- });
- session.executeCommand(removeFile1Consumer1ImportRequest);
- session.executeCommand(changeModuleFile1ShapeRequest1);
- sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer2] }]);
-
- // Add the import statements back to file2
- const addFile2ImportRequest = makeSessionRequest(server.CommandNames.Change, {
- file: file1Consumer1.path,
- line: 1,
- offset: 1,
- endLine: 1,
- endOffset: 1,
- insertString: `import {Foo} from "./moduleFile1";`
- });
- session.executeCommand(addFile2ImportRequest);
-
- // Change the content of file1 to `export var T2: string;export var T: number;export function Foo() { };`
- const changeModuleFile1ShapeRequest2 = makeSessionRequest(server.CommandNames.Change, {
- file: moduleFile1.path,
- line: 1,
- offset: 1,
- endLine: 1,
- endOffset: 1,
- insertString: `export var T2: string;`
- });
- session.executeCommand(changeModuleFile1ShapeRequest2);
- sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]);
- });
-
- it("should be up-to-date with changes made in non-open files", () => {
- const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]);
- const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
- const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
-
- openFilesForSession([moduleFile1], session);
-
- // Send an initial compileOnSave request
- sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]);
-
- file1Consumer1.content = `let y = 10;`;
- host.reloadFS([moduleFile1, file1Consumer1, file1Consumer2, configFile, libFile]);
- host.triggerFileWatcherCallback(file1Consumer1.path, /*removed*/ false);
-
- session.executeCommand(changeModuleFile1ShapeRequest1);
- sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer2] }]);
- });
-
- it("should be up-to-date with deleted files", () => {
- const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]);
- const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
- const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
-
- openFilesForSession([moduleFile1], session);
- sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]);
-
- session.executeCommand(changeModuleFile1ShapeRequest1);
- // Delete file1Consumer2
- host.reloadFS([moduleFile1, file1Consumer1, configFile, libFile]);
- host.triggerFileWatcherCallback(file1Consumer2.path, /*removed*/ true);
- sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]);
- });
-
- it("should be up-to-date with newly created files", () => {
- const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]);
- const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
- const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
-
- openFilesForSession([moduleFile1], session);
- sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]);
-
- const file1Consumer3: FileOrFolder = {
- path: "/a/b/file1Consumer3.ts",
- content: `import {Foo} from "./moduleFile1"; let y = Foo();`
- };
- host.reloadFS([moduleFile1, file1Consumer1, file1Consumer2, file1Consumer3, globalFile3, configFile, libFile]);
- host.triggerDirectoryWatcherCallback(ts.getDirectoryPath(file1Consumer3.path), file1Consumer3.path);
- host.runQueuedTimeoutCallbacks();
- session.executeCommand(changeModuleFile1ShapeRequest1);
- sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2, file1Consumer3] }]);
- });
-
- it("should detect changes in non-root files", () => {
- moduleFile1 = {
- path: "/a/b/moduleFile1.ts",
- content: "export function Foo() { };"
- };
-
- file1Consumer1 = {
- path: "/a/b/file1Consumer1.ts",
- content: `import {Foo} from "./moduleFile1"; let y = Foo();`
- };
-
- configFile = {
- path: "/a/b/tsconfig.json",
- content: `{
- "compileOnSave": true,
- "files": ["${file1Consumer1.path}"]
- }`
- };
-
- const host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]);
- const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
- const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
-
- openFilesForSession([moduleFile1, file1Consumer1], session);
- sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]);
-
- // change file1 shape now, and verify both files are affected
- session.executeCommand(changeModuleFile1ShapeRequest1);
- sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]);
-
- // change file1 internal, and verify only file1 is affected
- session.executeCommand(changeModuleFile1InternalRequest1);
- sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]);
- });
-
- it("should return all files if a global file changed shape", () => {
- const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]);
- const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
- const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
-
- openFilesForSession([globalFile3], session);
- const changeGlobalFile3ShapeRequest = makeSessionRequest(server.CommandNames.Change, {
- file: globalFile3.path,
- line: 1,
- offset: 1,
- endLine: 1,
- endOffset: 1,
- insertString: `var T2: string;`
- });
-
- // check after file1 shape changes
- session.executeCommand(changeGlobalFile3ShapeRequest);
- const globalFile3FileListRequest = makeSessionRequest(server.CommandNames.CompileOnSaveAffectedFileList, { file: globalFile3.path });
- sendAffectedFileRequestAndCheckResult(session, globalFile3FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2] }]);
- });
-
- it("should return empty array if CompileOnSave is not enabled", () => {
- configFile = {
- path: "/a/b/tsconfig.json",
- content: `{}`
- };
-
- const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile, libFile]);
- const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
- const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
- openFilesForSession([moduleFile1], session);
- sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, []);
- });
-
- it("should always return the file itself if '--isolatedModules' is specified", () => {
- configFile = {
- path: "/a/b/tsconfig.json",
- content: `{
- "compileOnSave": true,
- "compilerOptions": {
- "isolatedModules": true
- }
- }`
- };
-
- const host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]);
- const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
- const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
- openFilesForSession([moduleFile1], session);
-
- const file1ChangeShapeRequest = makeSessionRequest(server.CommandNames.Change, {
- file: moduleFile1.path,
- line: 1,
- offset: 27,
- endLine: 1,
- endOffset: 27,
- insertString: `Point,`
- });
- session.executeCommand(file1ChangeShapeRequest);
- sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]);
- });
-
- it("should always return the file itself if '--out' or '--outFile' is specified", () => {
- configFile = {
- path: "/a/b/tsconfig.json",
- content: `{
- "compileOnSave": true,
- "compilerOptions": {
- "module": "system",
- "outFile": "/a/b/out.js"
- }
- }`
- };
-
- const host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]);
- const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
- const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
- openFilesForSession([moduleFile1], session);
-
- const file1ChangeShapeRequest = makeSessionRequest(server.CommandNames.Change, {
- file: moduleFile1.path,
- line: 1,
- offset: 27,
- endLine: 1,
- endOffset: 27,
- insertString: `Point,`
- });
- session.executeCommand(file1ChangeShapeRequest);
- sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]);
- });
-
- it("should return cascaded affected file list", () => {
- const file1Consumer1Consumer1: FileOrFolder = {
- path: "/a/b/file1Consumer1Consumer1.ts",
- content: `import {y} from "./file1Consumer1";`
- };
- const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer1Consumer1, globalFile3, configFile, libFile]);
- const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
- const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
-
- openFilesForSession([moduleFile1, file1Consumer1], session);
- sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer1Consumer1] }]);
-
- const changeFile1Consumer1ShapeRequest = makeSessionRequest(server.CommandNames.Change, {
- file: file1Consumer1.path,
- line: 2,
- offset: 1,
- endLine: 2,
- endOffset: 1,
- insertString: `export var T: number;`
- });
- session.executeCommand(changeModuleFile1ShapeRequest1);
- session.executeCommand(changeFile1Consumer1ShapeRequest);
- sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer1Consumer1] }]);
- });
-
- it("should work fine for files with circular references", () => {
- const file1: FileOrFolder = {
- path: "/a/b/file1.ts",
- content: `
- ///
- export var t1 = 10;`
- };
- const file2: FileOrFolder = {
- path: "/a/b/file2.ts",
- content: `
- ///
- export var t2 = 10;`
- };
- const host = createServerHost([file1, file2, configFile]);
- const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
- const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
-
- openFilesForSession([file1, file2], session);
- const file1AffectedListRequest = makeSessionRequest(server.CommandNames.CompileOnSaveAffectedFileList, { file: file1.path });
- sendAffectedFileRequestAndCheckResult(session, file1AffectedListRequest, [{ projectFileName: configFile.path, files: [file1, file2] }]);
- });
-
- it("should return results for all projects if not specifying projectFileName", () => {
- const file1: FileOrFolder = { path: "/a/b/file1.ts", content: "export var t = 10;" };
- const file2: FileOrFolder = { path: "/a/b/file2.ts", content: `import {t} from "./file1"; var t2 = 11;` };
- const file3: FileOrFolder = { path: "/a/c/file2.ts", content: `import {t} from "../b/file1"; var t3 = 11;` };
- const configFile1: FileOrFolder = { path: "/a/b/tsconfig.json", content: `{ "compileOnSave": true }` };
- const configFile2: FileOrFolder = { path: "/a/c/tsconfig.json", content: `{ "compileOnSave": true }` };
-
- const host = createServerHost([file1, file2, file3, configFile1, configFile2]);
- const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
- const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
-
- openFilesForSession([file1, file2, file3], session);
- const file1AffectedListRequest = makeSessionRequest(server.CommandNames.CompileOnSaveAffectedFileList, { file: file1.path });
-
- sendAffectedFileRequestAndCheckResult(session, file1AffectedListRequest, [
- { projectFileName: configFile1.path, files: [file1, file2] },
- { projectFileName: configFile2.path, files: [file1, file3] }
- ]);
- });
- });
- });
-
- describe("EmitFile test", () => {
- it("should emit specified file", () => {
- const file1 = {
- path: "/a/b/f1.ts",
- content: `export function Foo() { return 10; }`
- };
- const file2 = {
- path: "/a/b/f2.ts",
- content: `import {Foo} from "./f1"; let y = Foo();`
- };
- const configFile = {
- path: "/a/b/tsconfig.json",
- content: `{}`
- };
- const host = createServerHost([file1, file2, configFile, libFile]);
- const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
- const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
-
- openFilesForSession([file1, file2], session);
- const compileFileRequest = makeSessionRequest(server.CommandNames.CompileOnSaveEmitFile, { file: file1.path, projectFileName: configFile.path });
- session.executeCommand(compileFileRequest);
-
- const expectedEmittedFileName = "/a/b/f1.js";
- assert.isTrue(host.fileExists(expectedEmittedFileName));
- assert.equal(host.readFile(expectedEmittedFileName), `"use strict";\r\nfunction Foo() { return 10; }\r\nexports.Foo = Foo;\r\n`);
- });
- });
-
- describe("typings installer", () => {
- it("configured projects (tsd installed) 1", () => {
- const file1 = {
- path: "/a/b/app.js",
- content: ""
- };
- const tsconfig = {
- path: "/a/b/tsconfig.json",
- content: JSON.stringify({
- compilerOptions: {
- allowJs: true
- },
- typingOptions: {
- enableAutoDiscovery: true
- }
- })
- };
- const packageJson = {
- path: "/a/b/package.json",
- content: JSON.stringify({
- name: "test",
- dependencies: {
- jquery: "^3.1.0"
- }
- })
- };
-
- const jquery = {
- path: "/a/data/typings/jquery/jquery.d.ts",
- content: "declare const $: { x: number }"
- };
-
- const host = createServerHost([file1, tsconfig, packageJson]);
- const installer = new TestTypingsInstaller("/a/data/", host);
- const projectService = createProjectService(host, { useSingleInferredProject: true, typingsInstaller: installer });
- projectService.openClientFile(file1.path);
-
- checkNumberOfProjects(projectService, { configuredProjects: 1 });
- const p = projectService.configuredProjects[0];
- checkProjectActualFiles(p, [file1.path]);
-
- assert(host.fileExists(combinePaths(installer.globalTypingsCacheLocation, "tsd.json")));
-
- installer.runPostInstallActions(t => {
- assert.deepEqual(t, ["jquery"]);
- host.createFileOrFolder(jquery, /*createParentDirectory*/ true);
- return ["jquery/jquery.d.ts"];
- });
- checkNumberOfProjects(projectService, { configuredProjects: 1 });
- checkProjectActualFiles(p, [file1.path, jquery.path]);
- });
-
- it("inferred project (tsd installed)", () => {
- const file1 = {
- path: "/a/b/app.js",
- content: ""
- };
- const packageJson = {
- path: "/a/b/package.json",
- content: JSON.stringify({
- name: "test",
- dependencies: {
- jquery: "^3.1.0"
- }
- })
- };
-
- const jquery = {
- path: "/a/data/typings/jquery/jquery.d.ts",
- content: "declare const $: { x: number }"
- };
- const host = createServerHost([file1, packageJson]);
- const installer = new TestTypingsInstaller("/a/data/", host);
-
- const projectService = createProjectService(host, { useSingleInferredProject: true, typingsInstaller: installer });
- projectService.openClientFile(file1.path);
-
- checkNumberOfProjects(projectService, { inferredProjects: 1 });
- const p = projectService.inferredProjects[0];
- checkProjectActualFiles(p, [file1.path]);
-
- assert(host.fileExists(combinePaths(installer.globalTypingsCacheLocation, "tsd.json")));
-
- installer.runPostInstallActions(t => {
- assert.deepEqual(t, ["jquery"]);
- host.createFileOrFolder(jquery, /*createParentDirectory*/ true);
- return ["jquery/jquery.d.ts"];
- });
- checkNumberOfProjects(projectService, { inferredProjects: 1 });
- checkProjectActualFiles(p, [file1.path, jquery.path]);
- });
-
- it("external project - no typing options, no .d.ts/js files", () => {
- const file1 = {
- path: "/a/b/app.ts",
- content: ""
- };
- const host = createServerHost([file1]);
- const installer = new (class extends TestTypingsInstaller {
- constructor() {
- super("", host);
- }
- enqueueInstallTypingsRequest() {
- assert(false, "auto discovery should not be enabled");
- }
- })();
-
- const projectFileName = "/a/app/test.csproj";
- const projectService = createProjectService(host, { typingsInstaller: installer });
- projectService.openExternalProject({
- projectFileName,
- options: {},
- rootFiles: [toExternalFile(file1.path)]
- });
- // by default auto discovery will kick in if project contain only .js/.d.ts files
- // in this case project contain only ts files - no auto discovery
- projectService.checkNumberOfProjects({ externalProjects: 1 });
- });
-
- it("external project - no autoDiscovery in typing options, no .d.ts/js files", () => {
- const file1 = {
- path: "/a/b/app.ts",
- content: ""
- };
- const host = createServerHost([file1]);
- const installer = new (class extends TestTypingsInstaller {
- constructor() {
- super("", host);
- }
- enqueueInstallTypingsRequest() {
- assert(false, "auto discovery should not be enabled");
- }
- })();
-
- const projectFileName = "/a/app/test.csproj";
- const projectService = createProjectService(host, { typingsInstaller: installer });
- projectService.openExternalProject({
- projectFileName,
- options: {},
- rootFiles: [toExternalFile(file1.path)],
- typingOptions: { include: ["jquery"] }
- });
- // by default auto discovery will kick in if project contain only .js/.d.ts files
- // in this case project contain only ts files - no auto discovery even if typing options is set
- projectService.checkNumberOfProjects({ externalProjects: 1 });
- });
-
- it("external project - autoDiscovery = true, no .d.ts/js files", () => {
- const file1 = {
- path: "/a/b/app.ts",
- content: ""
- };
- const host = createServerHost([file1]);
- let enqueueIsCalled = false;
- let runTsdIsCalled = false;
- const installer = new (class extends TestTypingsInstaller {
- constructor() {
- super("", host);
- }
- enqueueInstallTypingsRequest(project: server.Project, typingOptions: TypingOptions) {
- enqueueIsCalled = true;
- super.enqueueInstallTypingsRequest(project, typingOptions);
- }
- runTsd(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void): void {
- assert.deepEqual(typingsToInstall, ["node"]);
- runTsdIsCalled = true;
- super.runTsd(cachePath, typingsToInstall, postInstallAction);
- }
- })();
-
- const projectFileName = "/a/app/test.csproj";
- const projectService = createProjectService(host, { typingsInstaller: installer });
- projectService.openExternalProject({
- projectFileName,
- options: {},
- rootFiles: [toExternalFile(file1.path)],
- typingOptions: { enableAutoDiscovery: true, include: ["node"] }
- });
- // autoDiscovery is set in typing options - use it even if project contains only .ts files
- projectService.checkNumberOfProjects({ externalProjects: 1 });
- assert.isTrue(enqueueIsCalled, "expected 'enqueueIsCalled' to be true");
- assert.isTrue(runTsdIsCalled, "expected 'runTsdIsCalled' to be true");
- });
- });
-
- describe("Project errors", () => {
- function checkProjectErrors(projectFiles: server.ProjectFilesWithTSDiagnostics, expectedErrors: string[]) {
- assert.isTrue(projectFiles !== undefined, "missing project files");
- const errors = projectFiles.projectErrors;
- assert.equal(errors ? errors.length : 0, expectedErrors.length, `expected ${expectedErrors.length} error in the list`);
- if (expectedErrors.length) {
- for (let i = 0; i < errors.length; i++) {
- const actualMessage = flattenDiagnosticMessageText(errors[i].messageText, "\n");
- const expectedMessage = expectedErrors[i];
- assert.isTrue(actualMessage.indexOf(expectedMessage) === 0, `error message does not match, expected ${actualMessage} to start with ${expectedMessage}`);
- }
- }
- }
-
- it("external project - diagnostics for missing files", () => {
- const file1 = {
- path: "/a/b/app.ts",
- content: ""
- };
- const file2 = {
- path: "/a/b/lib.ts",
- content: ""
- };
- // only file1 exists - expect error
- const host = createServerHost([file1]);
- const projectService = createProjectService(host);
- const projectFileName = "/a/b/test.csproj";
-
- {
- projectService.openExternalProject({
- projectFileName,
- options: {},
- rootFiles: toExternalFiles([file1.path, file2.path])
- });
-
- projectService.checkNumberOfProjects({ externalProjects: 1 });
- const knownProjects = projectService.synchronizeProjectList([]);
- checkProjectErrors(knownProjects[0], ["File '/a/b/lib.ts' not found."]);
- }
- // only file2 exists - expect error
- host.reloadFS([file2]);
- {
- projectService.openExternalProject({
- projectFileName,
- options: {},
- rootFiles: toExternalFiles([file1.path, file2.path])
- });
- projectService.checkNumberOfProjects({ externalProjects: 1 });
- const knownProjects = projectService.synchronizeProjectList([]);
- checkProjectErrors(knownProjects[0], ["File '/a/b/app.ts' not found."]);
- }
-
- // both files exist - expect no errors
- host.reloadFS([file1, file2]);
- {
- projectService.openExternalProject({
- projectFileName,
- options: {},
- rootFiles: toExternalFiles([file1.path, file2.path])
- });
-
- projectService.checkNumberOfProjects({ externalProjects: 1 });
- const knownProjects = projectService.synchronizeProjectList([]);
- checkProjectErrors(knownProjects[0], []);
- }
- });
-
- it("configured projects - diagnostics for missing files", () => {
- const file1 = {
- path: "/a/b/app.ts",
- content: ""
- };
- const file2 = {
- path: "/a/b/lib.ts",
- content: ""
- };
- const config = {
- path: "/a/b/tsconfig.json",
- content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) })
- };
- const host = createServerHost([file1, config]);
- const projectService = createProjectService(host);
-
- projectService.openClientFile(file1.path);
- projectService.checkNumberOfProjects({ configuredProjects: 1 });
- checkProjectErrors(projectService.synchronizeProjectList([])[0], ["File '/a/b/lib.ts' not found."]);
-
- host.reloadFS([file1, file2, config]);
-
- projectService.openClientFile(file1.path);
- projectService.checkNumberOfProjects({ configuredProjects: 1 });
- checkProjectErrors(projectService.synchronizeProjectList([])[0], []);
- });
-
- it("configured projects - diagnostics for corrupted config 1", () => {
- const file1 = {
- path: "/a/b/app.ts",
- content: ""
- };
- const file2 = {
- path: "/a/b/lib.ts",
- content: ""
- };
- const correctConfig = {
- path: "/a/b/tsconfig.json",
- content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) })
- };
- const corruptedConfig = {
- path: correctConfig.path,
- content: correctConfig.content.substr(1)
- };
- const host = createServerHost([file1, file2, corruptedConfig]);
- const projectService = createProjectService(host);
-
- projectService.openClientFile(file1.path);
- {
- projectService.checkNumberOfProjects({ configuredProjects: 1 });
- const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f);
- assert.isTrue(configuredProject !== undefined, "should find configured project");
- checkProjectErrors(configuredProject, [
- "')' expected.",
- "Declaration or statement expected.",
- "Declaration or statement expected.",
- "Failed to parse file '/a/b/tsconfig.json'"
- ]);
- }
- // fix config and trigger watcher
- host.reloadFS([file1, file2, correctConfig]);
- host.triggerFileWatcherCallback(correctConfig.path, /*false*/);
- {
- projectService.checkNumberOfProjects({ configuredProjects: 1 });
- const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f);
- assert.isTrue(configuredProject !== undefined, "should find configured project");
- checkProjectErrors(configuredProject, []);
- }
- });
-
- it("configured projects - diagnostics for corrupted config 2", () => {
- const file1 = {
- path: "/a/b/app.ts",
- content: ""
- };
- const file2 = {
- path: "/a/b/lib.ts",
- content: ""
- };
- const correctConfig = {
- path: "/a/b/tsconfig.json",
- content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) })
- };
- const corruptedConfig = {
- path: correctConfig.path,
- content: correctConfig.content.substr(1)
- };
- const host = createServerHost([file1, file2, correctConfig]);
- const projectService = createProjectService(host);
-
- projectService.openClientFile(file1.path);
- {
- projectService.checkNumberOfProjects({ configuredProjects: 1 });
- const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f);
- assert.isTrue(configuredProject !== undefined, "should find configured project");
- checkProjectErrors(configuredProject, []);
- }
- // break config and trigger watcher
- host.reloadFS([file1, file2, corruptedConfig]);
- host.triggerFileWatcherCallback(corruptedConfig.path, /*false*/);
- {
- projectService.checkNumberOfProjects({ configuredProjects: 1 });
- const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f);
- assert.isTrue(configuredProject !== undefined, "should find configured project");
- checkProjectErrors(configuredProject, [
- "')' expected.",
- "Declaration or statement expected.",
- "Declaration or statement expected.",
- "Failed to parse file '/a/b/tsconfig.json'"
- ]);
- }
- });
- });
-
describe("Proper errors", () => {
it("document is not contained in project", () => {
const file1 = {
diff --git a/src/harness/unittests/typingsInstaller.ts b/src/harness/unittests/typingsInstaller.ts
new file mode 100644
index 00000000000..18c1e89ec72
--- /dev/null
+++ b/src/harness/unittests/typingsInstaller.ts
@@ -0,0 +1,190 @@
+///
+///
+///
+
+namespace ts.projectSystem {
+ describe("typings installer", () => {
+ it("configured projects (tsd installed) 1", () => {
+ const file1 = {
+ path: "/a/b/app.js",
+ content: ""
+ };
+ const tsconfig = {
+ path: "/a/b/tsconfig.json",
+ content: JSON.stringify({
+ compilerOptions: {
+ allowJs: true
+ },
+ typingOptions: {
+ enableAutoDiscovery: true
+ }
+ })
+ };
+ const packageJson = {
+ path: "/a/b/package.json",
+ content: JSON.stringify({
+ name: "test",
+ dependencies: {
+ jquery: "^3.1.0"
+ }
+ })
+ };
+
+ const jquery = {
+ path: "/a/data/typings/jquery/jquery.d.ts",
+ content: "declare const $: { x: number }"
+ };
+
+ const host = createServerHost([file1, tsconfig, packageJson]);
+ const installer = new TestTypingsInstaller("/a/data/", host);
+ const projectService = createProjectService(host, { useSingleInferredProject: true, typingsInstaller: installer });
+ projectService.openClientFile(file1.path);
+
+ checkNumberOfProjects(projectService, { configuredProjects: 1 });
+ const p = projectService.configuredProjects[0];
+ checkProjectActualFiles(p, [file1.path]);
+
+ assert(host.fileExists(combinePaths(installer.globalTypingsCacheLocation, "tsd.json")));
+
+ installer.runPostInstallActions(t => {
+ assert.deepEqual(t, ["jquery"]);
+ host.createFileOrFolder(jquery, /*createParentDirectory*/ true);
+ return ["jquery/jquery.d.ts"];
+ });
+ checkNumberOfProjects(projectService, { configuredProjects: 1 });
+ checkProjectActualFiles(p, [file1.path, jquery.path]);
+ });
+
+ it("inferred project (tsd installed)", () => {
+ const file1 = {
+ path: "/a/b/app.js",
+ content: ""
+ };
+ const packageJson = {
+ path: "/a/b/package.json",
+ content: JSON.stringify({
+ name: "test",
+ dependencies: {
+ jquery: "^3.1.0"
+ }
+ })
+ };
+
+ const jquery = {
+ path: "/a/data/typings/jquery/jquery.d.ts",
+ content: "declare const $: { x: number }"
+ };
+ const host = createServerHost([file1, packageJson]);
+ const installer = new TestTypingsInstaller("/a/data/", host);
+
+ const projectService = createProjectService(host, { useSingleInferredProject: true, typingsInstaller: installer });
+ projectService.openClientFile(file1.path);
+
+ checkNumberOfProjects(projectService, { inferredProjects: 1 });
+ const p = projectService.inferredProjects[0];
+ checkProjectActualFiles(p, [file1.path]);
+
+ assert(host.fileExists(combinePaths(installer.globalTypingsCacheLocation, "tsd.json")));
+
+ installer.runPostInstallActions(t => {
+ assert.deepEqual(t, ["jquery"]);
+ host.createFileOrFolder(jquery, /*createParentDirectory*/ true);
+ return ["jquery/jquery.d.ts"];
+ });
+ checkNumberOfProjects(projectService, { inferredProjects: 1 });
+ checkProjectActualFiles(p, [file1.path, jquery.path]);
+ });
+
+ it("external project - no typing options, no .d.ts/js files", () => {
+ const file1 = {
+ path: "/a/b/app.ts",
+ content: ""
+ };
+ const host = createServerHost([file1]);
+ const installer = new (class extends TestTypingsInstaller {
+ constructor() {
+ super("", host);
+ }
+ enqueueInstallTypingsRequest() {
+ assert(false, "auto discovery should not be enabled");
+ }
+ })();
+
+ const projectFileName = "/a/app/test.csproj";
+ const projectService = createProjectService(host, { typingsInstaller: installer });
+ projectService.openExternalProject({
+ projectFileName,
+ options: {},
+ rootFiles: [toExternalFile(file1.path)]
+ });
+ // by default auto discovery will kick in if project contain only .js/.d.ts files
+ // in this case project contain only ts files - no auto discovery
+ projectService.checkNumberOfProjects({ externalProjects: 1 });
+ });
+
+ it("external project - no autoDiscovery in typing options, no .d.ts/js files", () => {
+ const file1 = {
+ path: "/a/b/app.ts",
+ content: ""
+ };
+ const host = createServerHost([file1]);
+ const installer = new (class extends TestTypingsInstaller {
+ constructor() {
+ super("", host);
+ }
+ enqueueInstallTypingsRequest() {
+ assert(false, "auto discovery should not be enabled");
+ }
+ })();
+
+ const projectFileName = "/a/app/test.csproj";
+ const projectService = createProjectService(host, { typingsInstaller: installer });
+ projectService.openExternalProject({
+ projectFileName,
+ options: {},
+ rootFiles: [toExternalFile(file1.path)],
+ typingOptions: { include: ["jquery"] }
+ });
+ // by default auto discovery will kick in if project contain only .js/.d.ts files
+ // in this case project contain only ts files - no auto discovery even if typing options is set
+ projectService.checkNumberOfProjects({ externalProjects: 1 });
+ });
+
+ it("external project - autoDiscovery = true, no .d.ts/js files", () => {
+ const file1 = {
+ path: "/a/b/app.ts",
+ content: ""
+ };
+ const host = createServerHost([file1]);
+ let enqueueIsCalled = false;
+ let runTsdIsCalled = false;
+ const installer = new (class extends TestTypingsInstaller {
+ constructor() {
+ super("", host);
+ }
+ enqueueInstallTypingsRequest(project: server.Project, typingOptions: TypingOptions) {
+ enqueueIsCalled = true;
+ super.enqueueInstallTypingsRequest(project, typingOptions);
+ }
+ runTsd(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void): void {
+ assert.deepEqual(typingsToInstall, ["node"]);
+ runTsdIsCalled = true;
+ super.runTsd(cachePath, typingsToInstall, postInstallAction);
+ }
+ })();
+
+ const projectFileName = "/a/app/test.csproj";
+ const projectService = createProjectService(host, { typingsInstaller: installer });
+ projectService.openExternalProject({
+ projectFileName,
+ options: {},
+ rootFiles: [toExternalFile(file1.path)],
+ typingOptions: { enableAutoDiscovery: true, include: ["node"] }
+ });
+ // autoDiscovery is set in typing options - use it even if project contains only .ts files
+ projectService.checkNumberOfProjects({ externalProjects: 1 });
+ assert.isTrue(enqueueIsCalled, "expected 'enqueueIsCalled' to be true");
+ assert.isTrue(runTsdIsCalled, "expected 'runTsdIsCalled' to be true");
+ });
+ });
+}
\ No newline at end of file
diff --git a/src/server/project.ts b/src/server/project.ts
index 6700b906179..73418607107 100644
--- a/src/server/project.ts
+++ b/src/server/project.ts
@@ -442,7 +442,7 @@ namespace ts.server {
// but that will only be one dependency.
// To avoid invernal conversion, the key of the referencedFiles map must be of type Path
const referencedFiles = createMap();
- if (sourceFile.imports) {
+ if (sourceFile.imports && sourceFile.imports.length > 0) {
const checker: TypeChecker = this.program.getTypeChecker();
for (const importName of sourceFile.imports) {
const symbol = checker.getSymbolAtLocation(importName);
@@ -458,7 +458,7 @@ namespace ts.server {
const currentDirectory = getDirectoryPath(path);
const getCanonicalFileName = createGetCanonicalFileName(this.projectService.host.useCaseSensitiveFileNames);
// Handle triple slash references
- if (sourceFile.referencedFiles) {
+ if (sourceFile.referencedFiles && sourceFile.referencedFiles.length > 0) {
for (const referencedFile of sourceFile.referencedFiles) {
const referencedPath = toPath(referencedFile.fileName, currentDirectory, getCanonicalFileName);
referencedFiles[referencedPath] = true;
@@ -479,7 +479,8 @@ namespace ts.server {
}
}
- return map(Object.keys(referencedFiles), key => key);
+ const allFileNames = map(Object.keys(referencedFiles), key => key);
+ return filter(allFileNames, file => this.projectService.host.fileExists(file));
}
// remove a root file from project
diff --git a/src/server/session.ts b/src/server/session.ts
index fb29ff40a10..3da87f40631 100644
--- a/src/server/session.ts
+++ b/src/server/session.ts
@@ -951,6 +951,10 @@ namespace ts.server {
const info = this.projectService.getScriptInfo(args.file);
const result: protocol.CompileOnSaveAffectedFileListSingleProject[] = [];
+ if (!info) {
+ return [];
+ }
+
// if specified a project, we only return affected file list in this project
const projectsToSearch = args.projectFileName ? [this.projectService.findProject(args.projectFileName)] : info.containingProjects;
for (const project of projectsToSearch) {