From c5e502009ffcf295876cb2f3029236edbea0047c Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Mon, 27 Nov 2017 15:00:05 -0800 Subject: [PATCH] Migrate tsserverProjectSystem to vfs --- scripts/typemock/src/timers.ts | 10 +- src/harness/fakes.ts | 61 +- src/harness/unittests/compileOnSave.ts | 2 + src/harness/unittests/extractTestHelpers.ts | 18 +- src/harness/unittests/telemetry.ts | 4 +- .../unittests/tsserverProjectSystem.ts | 11639 ++++++++-------- src/harness/unittests/typingsInstaller.ts | 3 +- src/harness/utils.ts | 48 + src/harness/vfs.ts | 3 +- src/harness/virtualFileSystemWithWatch.ts | 57 +- 10 files changed, 6025 insertions(+), 5820 deletions(-) diff --git a/scripts/typemock/src/timers.ts b/scripts/typemock/src/timers.ts index 7811290b6d3..2dd22781387 100644 --- a/scripts/typemock/src/timers.ts +++ b/scripts/typemock/src/timers.ts @@ -124,12 +124,16 @@ export class Timers { * - Use `Timers.MAX_DEPTH` to continue processing nested `setImmediate` calls up to the maximum depth. */ public advance(ms: number, maxDepth = 0): number { - if (ms <= 0) throw new TypeError("Argument 'ms' out of range."); + if (ms < 0) throw new TypeError("Argument 'ms' out of range."); if (maxDepth < 0) throw new TypeError("Argument 'maxDepth' out of range."); let count = 0; const endTime = this._time + (ms | 0); while (true) { - count += this.executeImmediates(maxDepth); + if (maxDepth >= 0) { + count += this.executeImmediates(maxDepth); + maxDepth--; + } + const dueTimer = this.dequeueIfBefore(endTime); if (dueTimer) { this._time = dueTimer.due; @@ -150,7 +154,7 @@ export class Timers { * - Use `Timers.MAX_DEPTH` to continue processing nested `setImmediate` calls up to the maximum depth. */ public advanceToEnd(maxDepth = 0) { - return this.remainingTime > 0 ? this.advance(this.remainingTime, maxDepth) : 0; + return this.advance(this.remainingTime, maxDepth); } /** diff --git a/src/harness/fakes.ts b/src/harness/fakes.ts index 6fae3658104..125f0ec8804 100644 --- a/src/harness/fakes.ts +++ b/src/harness/fakes.ts @@ -30,11 +30,18 @@ namespace fakes { * Indicates whether to include a bare _lib.d.ts_. */ lib?: boolean; + /** + * Indicates whether to use DOS paths by default. + */ + dos?: boolean; } - export class FakeServerHost implements ts.server.ServerHost, ts.FormatDiagnosticsHost { + export class FakeServerHost implements ts.server.ServerHost, ts.FormatDiagnosticsHost, ts.ModuleResolutionHost { + public static readonly dosExecutingFilePath = "c:/.ts/tsc.js"; public static readonly defaultExecutingFilePath = "/.ts/tsc.js"; + public static readonly dosDefaultCurrentDirectory = "c:/"; public static readonly defaultCurrentDirectory = "/"; + public static readonly dosSafeListPath = "c:/safelist.json"; public static readonly safeListPath = "/safelist.json"; public static readonly safeListContent = `{\n` + @@ -46,6 +53,7 @@ namespace fakes { ` "chroma": "chroma-js"\n` + `}`; + public static readonly dosLibPath = "c:/.ts/lib.d.ts"; public static readonly libPath = "/.ts/lib.d.ts"; public static readonly libContent = `/// \n` + @@ -64,22 +72,32 @@ namespace fakes { private static readonly processExitSentinel = new Error("System exit"); private readonly _output: string[] = []; + private readonly _trace: string[] = []; private readonly _executingFilePath: string; private readonly _getCanonicalFileName: (file: string) => string; constructor(options: FakeServerHostOptions = {}) { const { + dos = false, vfs: _vfs = {}, - executingFilePath = FakeServerHost.defaultExecutingFilePath, + executingFilePath = dos + ? FakeServerHost.dosExecutingFilePath + : FakeServerHost.defaultExecutingFilePath, newLine = "\n", safeList = false, lib = false } = options; - const { currentDirectory = FakeServerHost.defaultCurrentDirectory, useCaseSensitiveFileNames = false } = _vfs; + const { + currentDirectory = dos + ? FakeServerHost.dosDefaultCurrentDirectory + : FakeServerHost.defaultCurrentDirectory, + useCaseSensitiveFileNames = false + } = _vfs; - this.vfs = _vfs instanceof vfs.VirtualFileSystem ? _vfs : - new vfs.VirtualFileSystem(currentDirectory, useCaseSensitiveFileNames); + this.vfs = _vfs instanceof vfs.VirtualFileSystem + ? _vfs + : new vfs.VirtualFileSystem(currentDirectory, useCaseSensitiveFileNames); this.useCaseSensitiveFileNames = this.vfs.useCaseSensitiveFileNames; this.newLine = newLine; @@ -87,11 +105,15 @@ namespace fakes { this._getCanonicalFileName = ts.createGetCanonicalFileName(this.useCaseSensitiveFileNames); if (safeList) { - this.vfs.addFile(FakeServerHost.safeListPath, FakeServerHost.safeListContent); + this.vfs.addFile( + dos ? FakeServerHost.dosSafeListPath : FakeServerHost.safeListPath, + FakeServerHost.safeListContent); } if (lib) { - this.vfs.addFile(FakeServerHost.libPath, FakeServerHost.libContent); + this.vfs.addFile( + dos ? FakeServerHost.dosLibPath : FakeServerHost.libPath, + FakeServerHost.libContent); } } @@ -143,6 +165,12 @@ namespace fakes { } // #endregion DirectoryStructureHost members + // #region ModuleResolutionHost members + public trace(message: string) { + this._trace.push(message); + } + // #endregion + // #region System members public readonly args: string[] = []; @@ -226,6 +254,14 @@ namespace fakes { this._output.length = 0; } + public getTrace(): ReadonlyArray { + return this._trace; + } + + public clearTrace() { + this._trace.length = 0; + } + public checkTimeoutQueueLength(expected: number) { const callbacksCount = this.timers.getPending({ kind: "timeout", ms: this.timers.remainingTime }).length; assert.equal(callbacksCount, expected, `expected ${expected} timeout callbacks queued but found ${callbacksCount}.`); @@ -236,6 +272,17 @@ namespace fakes { this.runQueuedTimeoutCallbacks(); } + public runQueuedImmediateCallbacks() { + try { + this.timers.executeImmediates(); + } + catch (e) { + if (e !== FakeServerHost.processExitSentinel) { + throw e; + } + } + } + public runQueuedTimeoutCallbacks() { try { this.timers.advanceToEnd(); diff --git a/src/harness/unittests/compileOnSave.ts b/src/harness/unittests/compileOnSave.ts index d462d55b4be..84c488770db 100644 --- a/src/harness/unittests/compileOnSave.ts +++ b/src/harness/unittests/compileOnSave.ts @@ -6,6 +6,8 @@ namespace ts.projectSystem { const nullCancellationToken = server.nullCancellationToken; + import libFile = ts.TestFSWithWatch.libFile; + import FileOrFolder = ts.TestFSWithWatch.FileOrFolder; function createTestTypingsInstaller(host: server.ServerHost) { return new TestTypingsInstaller("/a/data/", /*throttleLimit*/5, host); diff --git a/src/harness/unittests/extractTestHelpers.ts b/src/harness/unittests/extractTestHelpers.ts index a04f443c3c9..93ea2642675 100644 --- a/src/harness/unittests/extractTestHelpers.ts +++ b/src/harness/unittests/extractTestHelpers.ts @@ -1,5 +1,6 @@ /// /// +/// namespace ts { export interface Range { @@ -98,6 +99,19 @@ namespace ts { getCurrentDirectory: notImplemented, }; + function createServerHost(files: ts.TestFSWithWatch.FileOrFolder[], options?: Partial) { + const host = new fakes.FakeServerHost(options); + for (const file of files) { + if (isString(file.content)) { + host.vfs.writeFile(file.path, file.content); + } + else { + host.vfs.addDirectory(file.path); + } + } + return host; + } + export function testExtractSymbol(caption: string, text: string, baselineFolder: string, description: DiagnosticMessage, includeLib?: boolean) { const t = extractTest(text); const selectionRange = t.ranges.get("selection"); @@ -154,7 +168,7 @@ namespace ts { } function makeProgram(f: {path: string, content: string }, includeLib?: boolean) { - const host = projectSystem.createServerHost(includeLib ? [f, projectSystem.libFile] : [f]); // libFile is expensive to parse repeatedly - only test when required + const host = createServerHost(includeLib ? [f, ts.TestFSWithWatch.libFile] : [f]); // libFile is expensive to parse repeatedly - only test when required const projectService = projectSystem.createProjectService(host); projectService.openClientFile(f.path); const program = projectService.inferredProjects[0].getLanguageService().getProgram(); @@ -178,7 +192,7 @@ namespace ts { path: "/a.ts", content: t.source }; - const host = projectSystem.createServerHost([f, projectSystem.libFile]); + const host = ts.TestFSWithWatch.createServerHost([f, ts.TestFSWithWatch.libFile]); const projectService = projectSystem.createProjectService(host); projectService.openClientFile(f.path); const program = projectService.inferredProjects[0].getLanguageService().getProgram(); diff --git a/src/harness/unittests/telemetry.ts b/src/harness/unittests/telemetry.ts index 9bb2db73801..b33274da9ad 100644 --- a/src/harness/unittests/telemetry.ts +++ b/src/harness/unittests/telemetry.ts @@ -2,6 +2,8 @@ /// namespace ts.projectSystem { + import FileOrFolder = ts.TestFSWithWatch.FileOrFolder; + describe("project telemetry", () => { it("does nothing for inferred project", () => { const file = makeFile("/a.js"); @@ -235,7 +237,7 @@ namespace ts.projectSystem { }); }); - function makeFile(path: string, content: {} = ""): projectSystem.FileOrFolder { + function makeFile(path: string, content: {} = ""): FileOrFolder { return { path, content: isString(content) ? "" : JSON.stringify(content) }; } } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 35358e14ad4..af6c99d7d89 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -1,18 +1,21 @@ /// /// /// +/// +/// +/// namespace ts.projectSystem { + import spy = typemock.spy; + import dedent = utils.dedent; import TI = server.typingsInstaller; import protocol = server.protocol; import CommandNames = server.CommandNames; - export import TestServerHost = ts.TestFSWithWatch.TestServerHost; - export type FileOrFolder = ts.TestFSWithWatch.FileOrFolder; - export import createServerHost = ts.TestFSWithWatch.createServerHost; - export import checkFileNames = ts.TestFSWithWatch.checkFileNames; - export import libFile = ts.TestFSWithWatch.libFile; - export import checkWatchedFiles = ts.TestFSWithWatch.checkWatchedFiles; + import FileOrFolder = ts.TestFSWithWatch.FileOrFolder; + import checkFileNames = ts.TestFSWithWatch.checkFileNames; + import libFile = ts.TestFSWithWatch.libFile; + import checkWatchedFiles = ts.TestFSWithWatch.checkWatchedFiles; import checkWatchedDirectories = ts.TestFSWithWatch.checkWatchedDirectories; import safeList = ts.TestFSWithWatch.safeList; @@ -142,8 +145,8 @@ namespace ts.projectSystem { private events: server.ProjectServiceEvent[] = []; readonly session: TestSession; readonly service: server.ProjectService; - readonly host: projectSystem.TestServerHost; - constructor(files: projectSystem.FileOrFolder[]) { + readonly host: fakes.FakeServerHost; + constructor(files: FileOrFolder[]) { this.host = createServerHost(files); this.session = createSession(this.host, { canUseEvents: true, @@ -283,6 +286,7 @@ namespace ts.projectSystem { checkNumberOfProjects(this, count); } } + export function createProjectService(host: server.ServerHost, parameters: CreateProjectServiceParameters = {}, options?: Partial) { const cancellationToken = parameters.cancellationToken || server.nullCancellationToken; const logger = parameters.logger || nullLogger; @@ -456,11 +460,11 @@ namespace ts.projectSystem { verifyDiagnostics(actual, []); } - function assertEvent(actualOutput: string, expectedEvent: protocol.Event, host: TestServerHost) { + function assertEvent(actualOutput: string, expectedEvent: protocol.Event, host: ts.System) { assert.equal(actualOutput, server.formatMessage(expectedEvent, nullLogger, Utils.byteLength, host.newLine)); } - function checkErrorMessage(host: TestServerHost, eventName: "syntaxDiag" | "semanticDiag", diagnostics: protocol.DiagnosticEventBody) { + function checkErrorMessage(host: fakes.FakeServerHost, eventName: "syntaxDiag" | "semanticDiag", diagnostics: protocol.DiagnosticEventBody) { const outputs = host.getOutput(); assert.isTrue(outputs.length >= 1, outputs.toString()); const event: protocol.Event = { @@ -472,7 +476,7 @@ namespace ts.projectSystem { assertEvent(outputs[0], event, host); } - function checkCompleteEvent(host: TestServerHost, numberOfCurrentEvents: number, expectedSequenceId: number) { + function checkCompleteEvent(host: fakes.FakeServerHost, numberOfCurrentEvents: number, expectedSequenceId: number) { const outputs = host.getOutput(); assert.equal(outputs.length, numberOfCurrentEvents, outputs.toString()); const event: protocol.RequestCompletedEvent = { @@ -486,7 +490,7 @@ namespace ts.projectSystem { assertEvent(outputs[numberOfCurrentEvents - 1], event, host); } - function checkProjectUpdatedInBackgroundEvent(host: TestServerHost, openFiles: string[]) { + function checkProjectUpdatedInBackgroundEvent(host: fakes.FakeServerHost, openFiles: string[]) { const outputs = host.getOutput(); assert.equal(outputs.length, 1, outputs.toString()); const event: protocol.ProjectsUpdatedInBackgroundEvent = { @@ -500,5572 +504,5493 @@ namespace ts.projectSystem { assertEvent(outputs[0], event, host); } + function createServerHost(files: FileOrFolder[], options?: Partial) { + const host = new fakes.FakeServerHost(options); + for (const file of files) { + if (isString(file.content)) { + host.vfs.writeFile(file.path, file.content); + } + else { + host.vfs.addDirectory(file.path); + } + } + return host; + } + describe("tsserverProjectSystem", () => { - const commonFile1: FileOrFolder = { - path: "/a/b/commonFile1.ts", - content: "let x = 1" - }; - const commonFile2: FileOrFolder = { - path: "/a/b/commonFile2.ts", - content: "let y = 1" - }; - - it("create inferred project", () => { - const appFile: FileOrFolder = { - path: "/a/b/c/app.ts", - content: ` - import {f} from "./module" - console.log(f) - ` - }; - - const moduleFile: FileOrFolder = { - path: "/a/b/c/module.d.ts", - content: `export let x: number` - }; - const host = createServerHost([appFile, moduleFile, libFile]); - const projectService = createProjectService(host); - const { configFileName } = projectService.openClientFile(appFile.path); - - assert(!configFileName, `should not find config, got: '${configFileName}`); - checkNumberOfConfiguredProjects(projectService, 0); - checkNumberOfInferredProjects(projectService, 1); - - const project = projectService.inferredProjects[0]; - - checkFileNames("inferred project", project.getFileNames(), [appFile.path, libFile.path, moduleFile.path]); - const configFileLocations = ["/a/b/c/", "/a/b/", "/a/", "/"]; - const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]); - checkWatchedFiles(host, configFiles.concat(libFile.path, moduleFile.path)); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, ["/a/b/c", combinePaths(getDirectoryPath(appFile.path), nodeModulesAtTypes)], /*recursive*/ true); - }); - - it("can handle tsconfig file name with difference casing", () => { - const f1 = { - path: "/a/b/app.ts", - content: "let x = 1" - }; - const config = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ - include: [] - }) - }; - - const host = createServerHost([f1, config], { useCaseSensitiveFileNames: false }); - const service = createProjectService(host); - const upperCaseConfigFilePath = combinePaths(getDirectoryPath(config.path).toUpperCase(), getBaseFileName(config.path)); - service.openExternalProject({ - projectFileName: "/a/b/project.csproj", - rootFiles: toExternalFiles([f1.path, upperCaseConfigFilePath]), - options: {} - }); - service.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(service, 0), [upperCaseConfigFilePath]); - - service.openClientFile(f1.path); - service.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 }); - - checkProjectActualFiles(configuredProjectAt(service, 0), [upperCaseConfigFilePath]); - checkProjectActualFiles(service.inferredProjects[0], [f1.path]); - }); - - it("create configured project without file list", () => { - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: ` - { - "compilerOptions": {}, - "exclude": [ - "e" - ] - }` - }; - const file1: FileOrFolder = { - path: "/a/b/c/f1.ts", - content: "let x = 1" - }; - const file2: FileOrFolder = { - path: "/a/b/d/f2.ts", - content: "let y = 1" - }; - const file3: FileOrFolder = { - path: "/a/b/e/f3.ts", - content: "let z = 1" - }; - - const host = createServerHost([configFile, libFile, file1, file2, file3]); - const projectService = createProjectService(host); - const { configFileName, configFileErrors } = projectService.openClientFile(file1.path); - - assert(configFileName, "should find config file"); - assert.isTrue(!configFileErrors || configFileErrors.length === 0, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`); - checkNumberOfInferredProjects(projectService, 0); - checkNumberOfConfiguredProjects(projectService, 1); - - const project = configuredProjectAt(projectService, 0); - checkProjectActualFiles(project, [file1.path, libFile.path, file2.path, configFile.path]); - checkProjectRootFiles(project, [file1.path, file2.path]); - // watching all files except one that was open - checkWatchedFiles(host, [configFile.path, file2.path, libFile.path]); - const configFileDirectory = getDirectoryPath(configFile.path); - checkWatchedDirectories(host, [configFileDirectory, combinePaths(configFileDirectory, nodeModulesAtTypes)], /*recursive*/ true); - }); - - it("create configured project with the file list", () => { - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: ` - { - "compilerOptions": {}, - "include": ["*.ts"] - }` - }; - const file1: FileOrFolder = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const file2: FileOrFolder = { - path: "/a/b/f2.ts", - content: "let y = 1" - }; - const file3: FileOrFolder = { - path: "/a/b/c/f3.ts", - content: "let z = 1" - }; - - const host = createServerHost([configFile, libFile, file1, file2, file3]); - const projectService = createProjectService(host); - const { configFileName, configFileErrors } = projectService.openClientFile(file1.path); - - assert(configFileName, "should find config file"); - assert.isTrue(!configFileErrors || configFileErrors.length === 0, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`); - checkNumberOfInferredProjects(projectService, 0); - checkNumberOfConfiguredProjects(projectService, 1); - - const project = configuredProjectAt(projectService, 0); - checkProjectActualFiles(project, [file1.path, libFile.path, file2.path, configFile.path]); - checkProjectRootFiles(project, [file1.path, file2.path]); - // watching all files except one that was open - checkWatchedFiles(host, [configFile.path, file2.path, libFile.path]); - checkWatchedDirectories(host, [getDirectoryPath(configFile.path)], /*recursive*/ false); - }); - - it("add and then remove a config file in a folder with loose files", () => { - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{ - "files": ["commonFile1.ts"] - }` - }; - const filesWithoutConfig = [libFile, commonFile1, commonFile2]; - const host = createServerHost(filesWithoutConfig); - - const filesWithConfig = [libFile, commonFile1, commonFile2, configFile]; - const projectService = createProjectService(host); - projectService.openClientFile(commonFile1.path); - projectService.openClientFile(commonFile2.path); - - checkNumberOfInferredProjects(projectService, 2); - const configFileLocations = ["/", "/a/", "/a/b/"]; - const watchedFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]).concat(libFile.path); - checkWatchedFiles(host, watchedFiles); - - // Add a tsconfig file - host.reloadFS(filesWithConfig); - host.checkTimeoutQueueLengthAndRun(1); - checkNumberOfInferredProjects(projectService, 1); - checkNumberOfConfiguredProjects(projectService, 1); - checkWatchedFiles(host, watchedFiles); - - // remove the tsconfig file - host.reloadFS(filesWithoutConfig); - - checkNumberOfInferredProjects(projectService, 1); - host.checkTimeoutQueueLengthAndRun(1); // Refresh inferred projects - - checkNumberOfInferredProjects(projectService, 2); - checkNumberOfConfiguredProjects(projectService, 0); - checkWatchedFiles(host, watchedFiles); - }); - - it("add new files to a configured project without file list", () => { - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{}` - }; - const host = createServerHost([commonFile1, libFile, configFile]); - const projectService = createProjectService(host); - projectService.openClientFile(commonFile1.path); - const configFileDir = getDirectoryPath(configFile.path); - checkWatchedDirectories(host, [configFileDir, combinePaths(configFileDir, nodeModulesAtTypes)], /*recursive*/ true); - checkNumberOfConfiguredProjects(projectService, 1); - - const project = configuredProjectAt(projectService, 0); - checkProjectRootFiles(project, [commonFile1.path]); - - // add a new ts file - host.reloadFS([commonFile1, commonFile2, libFile, configFile]); - host.checkTimeoutQueueLengthAndRun(2); - // project service waits for 250ms to update the project structure, therefore the assertion needs to wait longer. - checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); - }); - - it("should ignore non-existing files specified in the config file", () => { - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": {}, - "files": [ - "commonFile1.ts", - "commonFile3.ts" - ] - }` - }; - const host = createServerHost([commonFile1, commonFile2, configFile]); - const projectService = createProjectService(host); - projectService.openClientFile(commonFile1.path); - projectService.openClientFile(commonFile2.path); - - checkNumberOfConfiguredProjects(projectService, 1); - const project = configuredProjectAt(projectService, 0); - checkProjectRootFiles(project, [commonFile1.path]); - checkNumberOfInferredProjects(projectService, 1); - }); - - it("remove not-listed external projects", () => { - const f1 = { - path: "/a/app.ts", - content: "let x = 1" - }; - const f2 = { - path: "/b/app.ts", - content: "let x = 1" - }; - const f3 = { - path: "/c/app.ts", - content: "let x = 1" - }; - const makeProject = (f: FileOrFolder) => ({ projectFileName: f.path + ".csproj", rootFiles: [toExternalFile(f.path)], options: {} }); - const p1 = makeProject(f1); - const p2 = makeProject(f2); - const p3 = makeProject(f3); - - const host = createServerHost([f1, f2, f3]); - const session = createSession(host); - - session.executeCommand({ - seq: 1, - type: "request", - command: "openExternalProjects", - arguments: { projects: [p1, p2] } - }); - - const projectService = session.getProjectService(); - checkNumberOfProjects(projectService, { externalProjects: 2 }); - assert.equal(projectService.externalProjects[0].getProjectName(), p1.projectFileName); - assert.equal(projectService.externalProjects[1].getProjectName(), p2.projectFileName); - - session.executeCommand({ - seq: 2, - type: "request", - command: "openExternalProjects", - arguments: { projects: [p1, p3] } - }); - checkNumberOfProjects(projectService, { externalProjects: 2 }); - assert.equal(projectService.externalProjects[0].getProjectName(), p1.projectFileName); - assert.equal(projectService.externalProjects[1].getProjectName(), p3.projectFileName); - - session.executeCommand({ - seq: 3, - type: "request", - command: "openExternalProjects", - arguments: { projects: [] } - }); - checkNumberOfProjects(projectService, { externalProjects: 0 }); - - session.executeCommand({ - seq: 3, - type: "request", - command: "openExternalProjects", - arguments: { projects: [p2] } - }); - assert.equal(projectService.externalProjects[0].getProjectName(), p2.projectFileName); - }); - - it("handle recreated files correctly", () => { - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{}` - }; - const host = createServerHost([commonFile1, commonFile2, configFile]); - const projectService = createProjectService(host); - projectService.openClientFile(commonFile1.path); - - checkNumberOfConfiguredProjects(projectService, 1); - const project = configuredProjectAt(projectService, 0); - checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); - - // delete commonFile2 - host.reloadFS([commonFile1, configFile]); - host.checkTimeoutQueueLengthAndRun(2); - checkProjectRootFiles(project, [commonFile1.path]); - - // re-add commonFile2 - host.reloadFS([commonFile1, commonFile2, configFile]); - host.checkTimeoutQueueLengthAndRun(2); - checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); - }); - - it("handles the missing files - that were added to program because they were added with /// { - const file1: FileOrFolder = { + describe("general", () => { + const commonFile1: FileOrFolder = { path: "/a/b/commonFile1.ts", - content: `/// - let x = y` + content: "let x = 1" + }; + const commonFile2: FileOrFolder = { + path: "/a/b/commonFile2.ts", + content: "let y = 1" }; - const host = createServerHost([file1, libFile]); - const session = createSession(host); - openFilesForSession([file1], session); - const projectService = session.getProjectService(); - checkNumberOfInferredProjects(projectService, 1); - const project = projectService.inferredProjects[0]; - checkProjectRootFiles(project, [file1.path]); - checkProjectActualFiles(project, [file1.path, libFile.path]); - const getErrRequest = makeSessionRequest( - server.CommandNames.SemanticDiagnosticsSync, - { file: file1.path } - ); + it("create inferred project", () => { + const host = new fakes.FakeServerHost({ lib: true }); + host.vfs.addFile("/a/b/c/app.ts", `import {f} from "./module"\nconsole.log(f)`); + host.vfs.addFile("/a/b/c/module.d.ts", `export let x: number`); - // Two errors: CommonFile2 not found and cannot find name y - let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; - verifyDiagnostics(diags, [ - { diagnosticMessage: Diagnostics.Cannot_find_name_0, errorTextArguments: ["y"] }, - { diagnosticMessage: Diagnostics.File_0_not_found, errorTextArguments: [commonFile2.path] } - ]); + const projectService = createProjectService(host); + const { configFileName } = projectService.openClientFile("/a/b/c/app.ts"); - host.reloadFS([file1, commonFile2, libFile]); - host.runQueuedTimeoutCallbacks(); - checkNumberOfInferredProjects(projectService, 1); - assert.strictEqual(projectService.inferredProjects[0], project, "Inferred project should be same"); - checkProjectRootFiles(project, [file1.path]); - checkProjectActualFiles(project, [file1.path, libFile.path, commonFile2.path]); - diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; - verifyNoDiagnostics(diags); - }); + assert(!configFileName, `should not find config, got: '${configFileName}`); + checkNumberOfConfiguredProjects(projectService, 0); + checkNumberOfInferredProjects(projectService, 1); - it("should create new inferred projects for files excluded from a configured project", () => { - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{ + const project = projectService.inferredProjects[0]; + + checkFileNames("inferred project", project.getFileNames(), ["/a/b/c/app.ts", fakes.FakeServerHost.libPath, "/a/b/c/module.d.ts"]); + const configFileLocations = ["/a/b/c/", "/a/b/", "/a/", "/"]; + const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]); + checkWatchedFiles(host, configFiles.concat(fakes.FakeServerHost.libPath, "/a/b/c/module.d.ts")); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, ["/a/b/c", combinePaths("/a/b/c/", nodeModulesAtTypes)], /*recursive*/ true); + }); + + it("can handle tsconfig file name with difference casing", () => { + const host = new fakes.FakeServerHost({ vfs: { useCaseSensitiveFileNames: false } }); + host.vfs.addFile("/a/b/app.ts", `let x = 1`); + host.vfs.addFile("/a/b/tsconfig.json", `{ "include": [] }`); + + const service = createProjectService(host); + service.openExternalProject({ + projectFileName: "/a/b/project.csproj", + rootFiles: toExternalFiles(["/a/b/app.ts", "/A/B/tsconfig.json"]), + options: {} + }); + service.checkNumberOfProjects({ configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(service, 0), ["/A/B/tsconfig.json"]); + + service.openClientFile("/a/b/app.ts"); + service.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 }); + + checkProjectActualFiles(configuredProjectAt(service, 0), ["/A/B/tsconfig.json"]); + checkProjectActualFiles(service.inferredProjects[0], ["/a/b/app.ts"]); + }); + + it("create configured project without file list", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: ` + { + "compilerOptions": {}, + "exclude": [ + "e" + ] + }` + }; + const file1: FileOrFolder = { + path: "/a/b/c/f1.ts", + content: "let x = 1" + }; + const file2: FileOrFolder = { + path: "/a/b/d/f2.ts", + content: "let y = 1" + }; + const file3: FileOrFolder = { + path: "/a/b/e/f3.ts", + content: "let z = 1" + }; + + const host = createServerHost([configFile, file1, file2, file3], { lib: true }); + const projectService = createProjectService(host); + const { configFileName, configFileErrors } = projectService.openClientFile(file1.path); + + assert(configFileName, "should find config file"); + assert.isTrue(!configFileErrors || configFileErrors.length === 0, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`); + checkNumberOfInferredProjects(projectService, 0); + checkNumberOfConfiguredProjects(projectService, 1); + + const project = configuredProjectAt(projectService, 0); + checkProjectActualFiles(project, [file1.path, fakes.FakeServerHost.libPath, file2.path, configFile.path]); + checkProjectRootFiles(project, [file1.path, file2.path]); + // watching all files except one that was open + checkWatchedFiles(host, [configFile.path, file2.path, fakes.FakeServerHost.libPath]); + const configFileDirectory = getDirectoryPath(configFile.path); + checkWatchedDirectories(host, [configFileDirectory, combinePaths(configFileDirectory, nodeModulesAtTypes)], /*recursive*/ true); + }); + + it("create configured project with the file list", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: ` + { + "compilerOptions": {}, + "include": ["*.ts"] + }` + }; + const file1: FileOrFolder = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2: FileOrFolder = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const file3: FileOrFolder = { + path: "/a/b/c/f3.ts", + content: "let z = 1" + }; + + const host = createServerHost([configFile, libFile, file1, file2, file3]); + const projectService = createProjectService(host); + const { configFileName, configFileErrors } = projectService.openClientFile(file1.path); + + assert(configFileName, "should find config file"); + assert.isTrue(!configFileErrors || configFileErrors.length === 0, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`); + checkNumberOfInferredProjects(projectService, 0); + checkNumberOfConfiguredProjects(projectService, 1); + + const project = configuredProjectAt(projectService, 0); + checkProjectActualFiles(project, [file1.path, fakes.FakeServerHost.libPath, file2.path, configFile.path]); + checkProjectRootFiles(project, [file1.path, file2.path]); + // watching all files except one that was open + checkWatchedFiles(host, [configFile.path, file2.path, fakes.FakeServerHost.libPath]); + checkWatchedDirectories(host, [getDirectoryPath(configFile.path)], /*recursive*/ false); + }); + + it("add and then remove a config file in a folder with loose files", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{ + "files": ["commonFile1.ts"] + }` + }; + const filesWithoutConfig = [libFile, commonFile1, commonFile2]; + const host = createServerHost(filesWithoutConfig); + + // const filesWithConfig = [libFile, commonFile1, commonFile2, configFile]; + const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + projectService.openClientFile(commonFile2.path); + + checkNumberOfInferredProjects(projectService, 2); + const configFileLocations = ["/", "/a/", "/a/b/"]; + const watchedFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]).concat(fakes.FakeServerHost.libPath); + checkWatchedFiles(host, watchedFiles); + + // Add a tsconfig file + host.vfs.addFile(configFile.path, configFile.content); + host.checkTimeoutQueueLengthAndRun(1); + checkNumberOfInferredProjects(projectService, 1); + checkNumberOfConfiguredProjects(projectService, 1); + checkWatchedFiles(host, watchedFiles); + + // remove the tsconfig file + host.vfs.removeFile(configFile.path); + + checkNumberOfInferredProjects(projectService, 1); + host.checkTimeoutQueueLengthAndRun(1); // Refresh inferred projects + + checkNumberOfInferredProjects(projectService, 2); + checkNumberOfConfiguredProjects(projectService, 0); + checkWatchedFiles(host, watchedFiles); + }); + + it("add new files to a configured project without file list", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = createServerHost([commonFile1, libFile, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + const configFileDir = getDirectoryPath(configFile.path); + checkWatchedDirectories(host, [configFileDir, combinePaths(configFileDir, nodeModulesAtTypes)], /*recursive*/ true); + checkNumberOfConfiguredProjects(projectService, 1); + + const project = configuredProjectAt(projectService, 0); + checkProjectRootFiles(project, [commonFile1.path]); + + // add a new ts file + host.vfs.addFile(commonFile2.path, commonFile2.content); + host.checkTimeoutQueueLengthAndRun(2); + // project service waits for 250ms to update the project structure, therefore the assertion needs to wait longer. + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + }); + + it("should ignore non-existing files specified in the config file", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "files": [ + "commonFile1.ts", + "commonFile3.ts" + ] + }` + }; + const host = createServerHost([commonFile1, commonFile2, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + projectService.openClientFile(commonFile2.path); + + checkNumberOfConfiguredProjects(projectService, 1); + const project = configuredProjectAt(projectService, 0); + checkProjectRootFiles(project, [commonFile1.path]); + checkNumberOfInferredProjects(projectService, 1); + }); + + it("remove not-listed external projects", () => { + const f1 = { + path: "/a/app.ts", + content: "let x = 1" + }; + const f2 = { + path: "/b/app.ts", + content: "let x = 1" + }; + const f3 = { + path: "/c/app.ts", + content: "let x = 1" + }; + const makeProject = (f: FileOrFolder) => ({ projectFileName: f.path + ".csproj", rootFiles: [toExternalFile(f.path)], options: {} }); + const p1 = makeProject(f1); + const p2 = makeProject(f2); + const p3 = makeProject(f3); + + const host = createServerHost([f1, f2, f3]); + const session = createSession(host); + + session.executeCommand({ + seq: 1, + type: "request", + command: "openExternalProjects", + arguments: { projects: [p1, p2] } + }); + + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { externalProjects: 2 }); + assert.equal(projectService.externalProjects[0].getProjectName(), p1.projectFileName); + assert.equal(projectService.externalProjects[1].getProjectName(), p2.projectFileName); + + session.executeCommand({ + seq: 2, + type: "request", + command: "openExternalProjects", + arguments: { projects: [p1, p3] } + }); + checkNumberOfProjects(projectService, { externalProjects: 2 }); + assert.equal(projectService.externalProjects[0].getProjectName(), p1.projectFileName); + assert.equal(projectService.externalProjects[1].getProjectName(), p3.projectFileName); + + session.executeCommand({ + seq: 3, + type: "request", + command: "openExternalProjects", + arguments: { projects: [] } + }); + checkNumberOfProjects(projectService, { externalProjects: 0 }); + + session.executeCommand({ + seq: 3, + type: "request", + command: "openExternalProjects", + arguments: { projects: [p2] } + }); + assert.equal(projectService.externalProjects[0].getProjectName(), p2.projectFileName); + }); + + it("handle recreated files correctly", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = createServerHost([commonFile1, commonFile2, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + + checkNumberOfConfiguredProjects(projectService, 1); + const project = configuredProjectAt(projectService, 0); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + + // delete commonFile2 + host.vfs.removeFile(commonFile2.path); + host.checkTimeoutQueueLengthAndRun(2); + checkProjectRootFiles(project, [commonFile1.path]); + + // re-add commonFile2 + host.vfs.addFile(commonFile2.path, commonFile2.content); + host.checkTimeoutQueueLengthAndRun(2); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + }); + + it("handles the missing files - that were added to program because they were added with /// { + const file1: FileOrFolder = { + path: "/a/b/commonFile1.ts", + content: `/// + let x = y` + }; + const host = createServerHost([file1, libFile]); + const session = createSession(host); + openFilesForSession([file1], session); + const projectService = session.getProjectService(); + + checkNumberOfInferredProjects(projectService, 1); + const project = projectService.inferredProjects[0]; + checkProjectRootFiles(project, [file1.path]); + checkProjectActualFiles(project, [file1.path, fakes.FakeServerHost.libPath]); + const getErrRequest = makeSessionRequest( + server.CommandNames.SemanticDiagnosticsSync, + { file: file1.path } + ); + + // Two errors: CommonFile2 not found and cannot find name y + let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; + verifyDiagnostics(diags, [ + { diagnosticMessage: Diagnostics.Cannot_find_name_0, errorTextArguments: ["y"] }, + { diagnosticMessage: Diagnostics.File_0_not_found, errorTextArguments: [commonFile2.path] } + ]); + + host.vfs.addFile(commonFile2.path, commonFile2.content); + host.runQueuedTimeoutCallbacks(); + checkNumberOfInferredProjects(projectService, 1); + assert.strictEqual(projectService.inferredProjects[0], project, "Inferred project should be same"); + checkProjectRootFiles(project, [file1.path]); + checkProjectActualFiles(project, [file1.path, fakes.FakeServerHost.libPath, commonFile2.path]); + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; + verifyNoDiagnostics(diags); + }); + + it("should create new inferred projects for files excluded from a configured project", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "files": ["${commonFile1.path}", "${commonFile2.path}"] + }` + }; + const files = [commonFile1, commonFile2, configFile]; + const host = createServerHost(files); + const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + + const project = configuredProjectAt(projectService, 0); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + configFile.content = `{ "compilerOptions": {}, - "files": ["${commonFile1.path}", "${commonFile2.path}"] - }` - }; - const files = [commonFile1, commonFile2, configFile]; - const host = createServerHost(files); - const projectService = createProjectService(host); - projectService.openClientFile(commonFile1.path); + "files": ["${commonFile1.path}"] + }`; + host.vfs.writeFile(configFile.path, configFile.content); - const project = configuredProjectAt(projectService, 0); - checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); - configFile.content = `{ - "compilerOptions": {}, - "files": ["${commonFile1.path}"] - }`; - host.reloadFS(files); + checkNumberOfConfiguredProjects(projectService, 1); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + host.checkTimeoutQueueLengthAndRun(2); // Update the configured project + refresh inferred projects + checkNumberOfConfiguredProjects(projectService, 1); + checkProjectRootFiles(project, [commonFile1.path]); - checkNumberOfConfiguredProjects(projectService, 1); - checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); - host.checkTimeoutQueueLengthAndRun(2); // Update the configured project + refresh inferred projects - checkNumberOfConfiguredProjects(projectService, 1); - checkProjectRootFiles(project, [commonFile1.path]); + projectService.openClientFile(commonFile2.path); + checkNumberOfInferredProjects(projectService, 1); + }); - projectService.openClientFile(commonFile2.path); - checkNumberOfInferredProjects(projectService, 1); - }); + it("files explicitly excluded in config file", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "exclude": ["/a/c"] + }` + }; + const excludedFile1: FileOrFolder = { + path: "/a/c/excluedFile1.ts", + content: `let t = 1;` + }; - it("files explicitly excluded in config file", () => { - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": {}, - "exclude": ["/a/c"] - }` - }; - const excludedFile1: FileOrFolder = { - path: "/a/c/excluedFile1.ts", - content: `let t = 1;` - }; + const host = createServerHost([commonFile1, commonFile2, excludedFile1, configFile]); + const projectService = createProjectService(host); - const host = createServerHost([commonFile1, commonFile2, excludedFile1, configFile]); - const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + checkNumberOfConfiguredProjects(projectService, 1); + const project = configuredProjectAt(projectService, 0); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + projectService.openClientFile(excludedFile1.path); + checkNumberOfInferredProjects(projectService, 1); + }); - projectService.openClientFile(commonFile1.path); - checkNumberOfConfiguredProjects(projectService, 1); - const project = configuredProjectAt(projectService, 0); - checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); - projectService.openClientFile(excludedFile1.path); - checkNumberOfInferredProjects(projectService, 1); - }); + it("should properly handle module resolution changes in config file", () => { + const file1: FileOrFolder = { + path: "/a/b/file1.ts", + content: `import { T } from "module1";` + }; + const nodeModuleFile: FileOrFolder = { + path: "/a/b/node_modules/module1.ts", + content: `export interface T {}` + }; + const classicModuleFile: FileOrFolder = { + path: "/a/module1.ts", + content: `export interface T {}` + }; + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "moduleResolution": "node" + }, + "files": ["${file1.path}"] + }` + }; + const files = [file1, nodeModuleFile, classicModuleFile, configFile]; + const host = createServerHost(files); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + projectService.openClientFile(nodeModuleFile.path); + projectService.openClientFile(classicModuleFile.path); - it("should properly handle module resolution changes in config file", () => { - const file1: FileOrFolder = { - path: "/a/b/file1.ts", - content: `import { T } from "module1";` - }; - const nodeModuleFile: FileOrFolder = { - path: "/a/b/node_modules/module1.ts", - content: `export interface T {}` - }; - const classicModuleFile: FileOrFolder = { - path: "/a/module1.ts", - content: `export interface T {}` - }; - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{ + checkNumberOfConfiguredProjects(projectService, 1); + const project = configuredProjectAt(projectService, 0); + checkProjectActualFiles(project, [file1.path, nodeModuleFile.path, configFile.path]); + checkNumberOfInferredProjects(projectService, 1); + + configFile.content = `{ "compilerOptions": { - "moduleResolution": "node" + "moduleResolution": "classic" }, "files": ["${file1.path}"] - }` - }; - const files = [file1, nodeModuleFile, classicModuleFile, configFile]; - const host = createServerHost(files); - const projectService = createProjectService(host); - projectService.openClientFile(file1.path); - projectService.openClientFile(nodeModuleFile.path); - projectService.openClientFile(classicModuleFile.path); - - checkNumberOfConfiguredProjects(projectService, 1); - const project = configuredProjectAt(projectService, 0); - checkProjectActualFiles(project, [file1.path, nodeModuleFile.path, configFile.path]); - checkNumberOfInferredProjects(projectService, 1); - - configFile.content = `{ - "compilerOptions": { - "moduleResolution": "classic" - }, - "files": ["${file1.path}"] - }`; - host.reloadFS(files); - host.checkTimeoutQueueLengthAndRun(2); - checkProjectActualFiles(project, [file1.path, classicModuleFile.path, configFile.path]); - checkNumberOfInferredProjects(projectService, 1); - }); - - it("should keep the configured project when the opened file is referenced by the project but not its root", () => { - const file1: FileOrFolder = { - path: "/a/b/main.ts", - content: "import { objA } from './obj-a';" - }; - const file2: FileOrFolder = { - path: "/a/b/obj-a.ts", - content: `export const objA = Object.assign({foo: "bar"}, {bar: "baz"});` - }; - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": { - "target": "es6" - }, - "files": [ "main.ts" ] - }` - }; - const host = createServerHost([file1, file2, configFile]); - const projectService = createProjectService(host); - projectService.openClientFile(file1.path); - projectService.closeClientFile(file1.path); - projectService.openClientFile(file2.path); - checkNumberOfConfiguredProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 0); - }); - - it("should keep the configured project when the opened file is referenced by the project but not its root", () => { - const file1: FileOrFolder = { - path: "/a/b/main.ts", - content: "import { objA } from './obj-a';" - }; - const file2: FileOrFolder = { - path: "/a/b/obj-a.ts", - content: `export const objA = Object.assign({foo: "bar"}, {bar: "baz"});` - }; - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": { - "target": "es6" - }, - "files": [ "main.ts" ] - }` - }; - const host = createServerHost([file1, file2, configFile]); - const projectService = createProjectService(host); - projectService.openClientFile(file1.path); - projectService.closeClientFile(file1.path); - projectService.openClientFile(file2.path); - checkNumberOfConfiguredProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 0); - }); - it("should tolerate config file errors and still try to build a project", () => { - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": { - "target": "es6", - "allowAnything": true - }, - "someOtherProperty": {} - }` - }; - const host = createServerHost([commonFile1, commonFile2, libFile, configFile]); - const projectService = createProjectService(host); - projectService.openClientFile(commonFile1.path); - checkNumberOfConfiguredProjects(projectService, 1); - checkProjectRootFiles(configuredProjectAt(projectService, 0), [commonFile1.path, commonFile2.path]); - }); - - it("should disable features when the files are too large", () => { - const file1 = { - path: "/a/b/f1.js", - content: "let x =1;", - fileSize: 10 * 1024 * 1024 - }; - const file2 = { - path: "/a/b/f2.js", - content: "let y =1;", - fileSize: 6 * 1024 * 1024 - }; - const file3 = { - path: "/a/b/f3.js", - content: "let y =1;", - fileSize: 6 * 1024 * 1024 - }; - - const proj1name = "proj1", proj2name = "proj2", proj3name = "proj3"; - - const host = createServerHost([file1, file2, file3]); - const projectService = createProjectService(host); - - projectService.openExternalProject({ rootFiles: toExternalFiles([file1.path]), options: {}, projectFileName: proj1name }); - const proj1 = projectService.findProject(proj1name); - assert.isTrue(proj1.languageServiceEnabled); - - projectService.openExternalProject({ rootFiles: toExternalFiles([file2.path]), options: {}, projectFileName: proj2name }); - const proj2 = projectService.findProject(proj2name); - assert.isTrue(proj2.languageServiceEnabled); - - projectService.openExternalProject({ rootFiles: toExternalFiles([file3.path]), options: {}, projectFileName: proj3name }); - const proj3 = projectService.findProject(proj3name); - assert.isFalse(proj3.languageServiceEnabled); - }); - - it("should use only one inferred project if 'useOneInferredProject' is set", () => { - const file1 = { - path: "/a/b/main.ts", - content: "let x =1;" - }; - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": { - "target": "es6" - }, - "files": [ "main.ts" ] - }` - }; - const file2 = { - path: "/a/c/main.ts", - content: "let x =1;" - }; - - const file3 = { - path: "/a/d/main.ts", - content: "let x =1;" - }; - - const host = createServerHost([file1, file2, file3, libFile]); - const projectService = createProjectService(host, { useSingleInferredProject: true }); - projectService.openClientFile(file1.path); - projectService.openClientFile(file2.path); - projectService.openClientFile(file3.path); - - checkNumberOfConfiguredProjects(projectService, 0); - checkNumberOfInferredProjects(projectService, 1); - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path, libFile.path]); - - - host.reloadFS([file1, configFile, file2, file3, libFile]); - host.checkTimeoutQueueLengthAndRun(1); - checkNumberOfConfiguredProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 1); - checkProjectActualFiles(projectService.inferredProjects[0], [file2.path, file3.path, libFile.path]); - }); - - it("should reuse same project if file is opened from the configured project that has no open files", () => { - const file1 = { - path: "/a/b/main.ts", - content: "let x =1;" - }; - const file2 = { - path: "/a/b/main2.ts", - content: "let y =1;" - }; - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": { - "target": "es6" - }, - "files": [ "main.ts", "main2.ts" ] - }` - }; - const host = createServerHost([file1, file2, configFile, libFile]); - const projectService = createProjectService(host, { useSingleInferredProject: true }); - projectService.openClientFile(file1.path); - checkNumberOfConfiguredProjects(projectService, 1); - const project = projectService.configuredProjects.get(configFile.path); - assert.isTrue(project.hasOpenRef()); // file1 - - projectService.closeClientFile(file1.path); - checkNumberOfConfiguredProjects(projectService, 1); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - assert.isFalse(project.hasOpenRef()); // No open files - assert.isFalse(project.isClosed()); - - projectService.openClientFile(file2.path); - checkNumberOfConfiguredProjects(projectService, 1); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - assert.isTrue(project.hasOpenRef()); // file2 - assert.isFalse(project.isClosed()); - }); - - it("should not close configured project after closing last open file, but should be closed on next file open if its not the file from same project", () => { - const file1 = { - path: "/a/b/main.ts", - content: "let x =1;" - }; - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": { - "target": "es6" - }, - "files": [ "main.ts" ] - }` - }; - const host = createServerHost([file1, configFile, libFile]); - const projectService = createProjectService(host, { useSingleInferredProject: true }); - projectService.openClientFile(file1.path); - checkNumberOfConfiguredProjects(projectService, 1); - const project = projectService.configuredProjects.get(configFile.path); - assert.isTrue(project.hasOpenRef()); // file1 - - projectService.closeClientFile(file1.path); - checkNumberOfConfiguredProjects(projectService, 1); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - assert.isFalse(project.hasOpenRef()); // No files - assert.isFalse(project.isClosed()); - - projectService.openClientFile(libFile.path); - checkNumberOfConfiguredProjects(projectService, 0); - assert.isFalse(project.hasOpenRef()); // No files + project closed - assert.isTrue(project.isClosed()); - }); - - it("should not close external project with no open files", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x =1;" - }; - const file2 = { - path: "/a/b/f2.ts", - content: "let y =1;" - }; - const externalProjectName = "externalproject"; - const host = createServerHost([file1, file2]); - const projectService = createProjectService(host); - projectService.openExternalProject({ - rootFiles: toExternalFiles([file1.path, file2.path]), - options: {}, - projectFileName: externalProjectName + }`; + host.vfs.writeFile(configFile.path, configFile.content); + host.checkTimeoutQueueLengthAndRun(2); + checkProjectActualFiles(project, [file1.path, classicModuleFile.path, configFile.path]); + checkNumberOfInferredProjects(projectService, 1); }); - checkNumberOfExternalProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 0); - - // open client file - should not lead to creation of inferred project - projectService.openClientFile(file1.path, file1.content); - checkNumberOfExternalProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 0); - - // close client file - external project should still exists - projectService.closeClientFile(file1.path); - checkNumberOfExternalProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 0); - - projectService.closeExternalProject(externalProjectName); - checkNumberOfExternalProjects(projectService, 0); - checkNumberOfInferredProjects(projectService, 0); - }); - - it("external project that included config files", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x =1;" - }; - const config1 = { - path: "/a/b/tsconfig.json", - content: JSON.stringify( - { - compilerOptions: {}, - files: ["f1.ts"] - } - ) - }; - const file2 = { - path: "/a/c/f2.ts", - content: "let y =1;" - }; - const config2 = { - path: "/a/c/tsconfig.json", - content: JSON.stringify( - { - compilerOptions: {}, - files: ["f2.ts"] - } - ) - }; - const file3 = { - path: "/a/d/f3.ts", - content: "let z =1;" - }; - const externalProjectName = "externalproject"; - const host = createServerHost([file1, file2, file3, config1, config2]); - const projectService = createProjectService(host); - projectService.openExternalProject({ - rootFiles: toExternalFiles([config1.path, config2.path, file3.path]), - options: {}, - projectFileName: externalProjectName - }); - - checkNumberOfProjects(projectService, { configuredProjects: 2 }); - const proj1 = projectService.configuredProjects.get(config1.path); - const proj2 = projectService.configuredProjects.get(config2.path); - assert.isDefined(proj1); - assert.isDefined(proj2); - - // open client file - should not lead to creation of inferred project - projectService.openClientFile(file1.path, file1.content); - checkNumberOfProjects(projectService, { configuredProjects: 2 }); - assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); - assert.strictEqual(projectService.configuredProjects.get(config2.path), proj2); - - projectService.openClientFile(file3.path, file3.content); - checkNumberOfProjects(projectService, { configuredProjects: 2, inferredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); - assert.strictEqual(projectService.configuredProjects.get(config2.path), proj2); - - projectService.closeExternalProject(externalProjectName); - // open file 'file1' from configured project keeps project alive - checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); - assert.isUndefined(projectService.configuredProjects.get(config2.path)); - - projectService.closeClientFile(file3.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); - assert.isUndefined(projectService.configuredProjects.get(config2.path)); - - projectService.closeClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); - assert.isUndefined(projectService.configuredProjects.get(config2.path)); - - projectService.openClientFile(file2.path, file2.content); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.isUndefined(projectService.configuredProjects.get(config1.path)); - assert.isDefined(projectService.configuredProjects.get(config2.path)); - - }); - - it("reload regular file after closing", () => { - const f1 = { - path: "/a/b/app.ts", - content: "x." - }; - const f2 = { - path: "/a/b/lib.ts", - content: "let x: number;" - }; - - const host = createServerHost([f1, f2, libFile]); - const service = createProjectService(host); - service.openExternalProject({ projectFileName: "/a/b/project", rootFiles: toExternalFiles([f1.path, f2.path]), options: {} }); - - service.openClientFile(f1.path); - service.openClientFile(f2.path, "let x: string"); - - service.checkNumberOfProjects({ externalProjects: 1 }); - checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, libFile.path]); - - const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, { includeExternalModuleExports: false }); - // should contain completions for string - assert.isTrue(completions1.entries.some(e => e.name === "charAt"), "should contain 'charAt'"); - assert.isFalse(completions1.entries.some(e => e.name === "toExponential"), "should not contain 'toExponential'"); - - service.closeClientFile(f2.path); - const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, { includeExternalModuleExports: false }); - // should contain completions for string - assert.isFalse(completions2.entries.some(e => e.name === "charAt"), "should not contain 'charAt'"); - assert.isTrue(completions2.entries.some(e => e.name === "toExponential"), "should contain 'toExponential'"); - }); - - it("clear mixed content file after closing", () => { - const f1 = { - path: "/a/b/app.ts", - content: " " - }; - const f2 = { - path: "/a/b/lib.html", - content: "" - }; - - const host = createServerHost([f1, f2, libFile]); - const service = createProjectService(host); - service.openExternalProject({ projectFileName: "/a/b/project", rootFiles: [{ fileName: f1.path }, { fileName: f2.path, hasMixedContent: true }], options: {} }); - - service.openClientFile(f1.path); - service.openClientFile(f2.path, "let somelongname: string"); - - service.checkNumberOfProjects({ externalProjects: 1 }); - checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, libFile.path]); - - const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, { includeExternalModuleExports: false }); - assert.isTrue(completions1.entries.some(e => e.name === "somelongname"), "should contain 'somelongname'"); - - service.closeClientFile(f2.path); - const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, { includeExternalModuleExports: false }); - assert.isFalse(completions2.entries.some(e => e.name === "somelongname"), "should not contain 'somelongname'"); - const sf2 = service.externalProjects[0].getLanguageService().getProgram().getSourceFile(f2.path); - assert.equal(sf2.text, ""); - }); - - - it("external project with included config file opened after configured project", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: {} }) - }; - const externalProjectName = "externalproject"; - const host = createServerHost([file1, configFile]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - - projectService.openExternalProject({ - rootFiles: toExternalFiles([configFile.path]), - options: {}, - projectFileName: externalProjectName - }); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - - projectService.closeClientFile(file1.path); - // configured project is alive since it is opened as part of external project - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - - projectService.closeExternalProject(externalProjectName); - checkNumberOfProjects(projectService, { configuredProjects: 0 }); - }); - - it("external project with included config file opened after configured project and then closed", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const file2 = { - path: "/a/f2.ts", - content: "let x = 1" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: {} }) - }; - const externalProjectName = "externalproject"; - const host = createServerHost([file1, file2, libFile, configFile]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project = projectService.configuredProjects.get(configFile.path); - - projectService.openExternalProject({ - rootFiles: toExternalFiles([configFile.path]), - options: {}, - projectFileName: externalProjectName - }); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - - projectService.closeExternalProject(externalProjectName); - // configured project is alive since file is still open - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - - projectService.closeClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - - projectService.openClientFile(file2.path); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - assert.isUndefined(projectService.configuredProjects.get(configFile.path)); - }); - - it("changes in closed files are reflected in project structure", () => { - const file1 = { - path: "/a/b/f1.ts", - content: `export * from "./f2"` - }; - const file2 = { - path: "/a/b/f2.ts", - content: `export let x = 1` - }; - const file3 = { - path: "/a/c/f3.ts", - content: `export let y = 1;` - }; - const host = createServerHost([file1, file2, file3]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - - checkNumberOfInferredProjects(projectService, 1); - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path]); - - projectService.openClientFile(file3.path); - checkNumberOfInferredProjects(projectService, 2); - checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); - - const modifiedFile2 = { - path: file2.path, - content: `export * from "../c/f3"` // now inferred project should inclule file3 - }; - - host.reloadFS([file1, modifiedFile2, file3]); - host.checkTimeoutQueueLengthAndRun(2); - checkNumberOfInferredProjects(projectService, 1); - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, modifiedFile2.path, file3.path]); - }); - - it("deleted files affect project structure", () => { - const file1 = { - path: "/a/b/f1.ts", - content: `export * from "./f2"` - }; - const file2 = { - path: "/a/b/f2.ts", - content: `export * from "../c/f3"` - }; - const file3 = { - path: "/a/c/f3.ts", - content: `export let y = 1;` - }; - const host = createServerHost([file1, file2, file3]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]); - - projectService.openClientFile(file3.path); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - - host.reloadFS([file1, file3]); - host.checkTimeoutQueueLengthAndRun(2); - - checkNumberOfProjects(projectService, { inferredProjects: 2 }); - - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); - checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); - }); - - it("ignores files excluded by a custom safe type list", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "export let x = 5" - }; - const office = { - path: "/lib/duckquack-3.min.js", - content: "whoa do @@ not parse me ok thanks!!!" - }; - const host = createServerHost([file1, office, customTypesMap]); - const projectService = createProjectService(host); - try { - projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, office.path]) }); - const proj = projectService.externalProjects[0]; - assert.deepEqual(proj.getFileNames(/*excludeFilesFromExternalLibraries*/ true), [file1.path]); - assert.deepEqual(proj.getTypeAcquisition().include, ["duck-types"]); - } finally { - projectService.resetSafeList(); - } - }); - - it("ignores files excluded by the default type list", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "export let x = 5" - }; - const minFile = { - path: "/c/moment.min.js", - content: "unspecified" - }; - const kendoFile1 = { - path: "/q/lib/kendo/kendo.all.min.js", - content: "unspecified" - }; - const kendoFile2 = { - path: "/q/lib/kendo/kendo.ui.min.js", - content: "unspecified" - }; - const officeFile1 = { - path: "/scripts/Office/1/excel-15.debug.js", - content: "unspecified" - }; - const officeFile2 = { - path: "/scripts/Office/1/powerpoint.js", - content: "unspecified" - }; - const files = [file1, minFile, kendoFile1, kendoFile2, officeFile1, officeFile2]; - const host = createServerHost(files); - const projectService = createProjectService(host); - try { - projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles(files.map(f => f.path)) }); - const proj = projectService.externalProjects[0]; - assert.deepEqual(proj.getFileNames(/*excludeFilesFromExternalLibraries*/ true), [file1.path]); - assert.deepEqual(proj.getTypeAcquisition().include, ["kendo-ui", "office"]); - } finally { - projectService.resetSafeList(); - } - }); - - it("removes version numbers correctly", () => { - const testData: [string, string][] = [ - ["jquery-max", "jquery-max"], - ["jquery.min", "jquery"], - ["jquery-min.4.2.3", "jquery"], - ["jquery.min.4.2.1", "jquery"], - ["minimum", "minimum"], - ["min", "min"], - ["min.3.2", "min"], - ["jquery", "jquery"] - ]; - for (const t of testData) { - assert.equal(removeMinAndVersionNumbers(t[0]), t[1], t[0]); - } - }); - - it("ignores files excluded by a legacy safe type list", () => { - const file1 = { - path: "/a/b/bliss.js", - content: "let x = 5" - }; - const file2 = { - path: "/a/b/foo.js", - content: "" - }; - const host = createServerHost([file1, file2, customTypesMap]); - const projectService = createProjectService(host); - try { - projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, file2.path]), typeAcquisition: { enable: true } }); - const proj = projectService.externalProjects[0]; - assert.deepEqual(proj.getFileNames(), [file2.path]); - } finally { - projectService.resetSafeList(); - } - }); - - it("open file become a part of configured project if it is referenced from root file", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "export let x = 5" - }; - const file2 = { - path: "/a/c/f2.ts", - content: `import {x} from "../b/f1"` - }; - const file3 = { - path: "/a/c/f3.ts", - content: "export let y = 1" - }; - const configFile = { - path: "/a/c/tsconfig.json", - content: JSON.stringify({ compilerOptions: {}, files: ["f2.ts", "f3.ts"] }) - }; - - const host = createServerHost([file1, file2, file3]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); - - projectService.openClientFile(file3.path); - checkNumberOfProjects(projectService, { inferredProjects: 2 }); - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); - checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); - - host.reloadFS([file1, file2, file3, configFile]); - host.checkTimeoutQueueLengthAndRun(1); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, file3.path, configFile.path]); - }); - - it("correctly migrate files between projects", () => { - const file1 = { - path: "/a/b/f1.ts", - content: ` - export * from "../c/f2"; - export * from "../d/f3";` - }; - const file2 = { - path: "/a/c/f2.ts", - content: "export let x = 1;" - }; - const file3 = { - path: "/a/d/f3.ts", - content: "export let y = 1;" - }; - const host = createServerHost([file1, file2, file3]); - const projectService = createProjectService(host); - - projectService.openClientFile(file2.path); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - checkProjectActualFiles(projectService.inferredProjects[0], [file2.path]); - - projectService.openClientFile(file3.path); - checkNumberOfProjects(projectService, { inferredProjects: 2 }); - checkProjectActualFiles(projectService.inferredProjects[0], [file2.path]); - checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); - - projectService.openClientFile(file1.path); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - checkProjectRootFiles(projectService.inferredProjects[0], [file1.path]); - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]); - - projectService.closeClientFile(file1.path); - checkNumberOfProjects(projectService, { inferredProjects: 2 }); - }); - - it("can correctly update configured project when set of root files has changed (new file on disk)", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const file2 = { - path: "/a/b/f2.ts", - content: "let y = 1" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: {} }) - }; - - const host = createServerHost([file1, configFile]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, configFile.path]); - - host.reloadFS([file1, file2, configFile]); - - host.checkTimeoutQueueLengthAndRun(2); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]); - }); - - it("can correctly update configured project when set of root files has changed (new file in list of files)", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const file2 = { - path: "/a/b/f2.ts", - content: "let y = 1" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts"] }) - }; - - const host = createServerHost([file1, file2, configFile]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, configFile.path]); - - const modifiedConfigFile = { - path: configFile.path, - content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) - }; - - host.reloadFS([file1, file2, modifiedConfigFile]); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - host.checkTimeoutQueueLengthAndRun(2); - checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]); - }); - - it("can update configured project when set of root files was not changed", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const file2 = { - path: "/a/b/f2.ts", - content: "let y = 1" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) - }; - - const host = createServerHost([file1, file2, configFile]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, configFile.path]); - - const modifiedConfigFile = { - path: configFile.path, - content: JSON.stringify({ compilerOptions: { outFile: "out.js" }, files: ["f1.ts", "f2.ts"] }) - }; - - host.reloadFS([file1, file2, modifiedConfigFile]); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]); - }); - - it("can correctly update external project when set of root files has changed", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const file2 = { - path: "/a/b/f2.ts", - content: "let y = 1" - }; - const host = createServerHost([file1, file2]); - const projectService = createProjectService(host); - - projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path]) }); - checkNumberOfProjects(projectService, { externalProjects: 1 }); - checkProjectActualFiles(projectService.externalProjects[0], [file1.path]); - - projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, file2.path]) }); - checkNumberOfProjects(projectService, { externalProjects: 1 }); - checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); - }); - - it("can update external project when set of root files was not changed", () => { - const file1 = { - path: "/a/b/f1.ts", - content: `export * from "m"` - }; - const file2 = { - path: "/a/b/f2.ts", - content: "export let y = 1" - }; - const file3 = { - path: "/a/m.ts", - content: "export let y = 1" - }; - - const host = createServerHost([file1, file2, file3]); - const projectService = createProjectService(host); - - projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ModuleResolutionKind.NodeJs }, rootFiles: toExternalFiles([file1.path, file2.path]) }); - checkNumberOfProjects(projectService, { externalProjects: 1 }); - checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); - checkProjectActualFiles(projectService.externalProjects[0], [file1.path, file2.path]); - - projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ModuleResolutionKind.Classic }, rootFiles: toExternalFiles([file1.path, file2.path]) }); - checkNumberOfProjects(projectService, { externalProjects: 1 }); - checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); - checkProjectActualFiles(projectService.externalProjects[0], [file1.path, file2.path, file3.path]); - }); - - it("regression test for crash in acquireOrUpdateDocument", () => { - const tsFile = { - fileName: "/a/b/file1.ts", - path: "/a/b/file1.ts", - content: "" - }; - const jsFile = { - path: "/a/b/file1.js", - content: "var x = 10;", - fileName: "/a/b/file1.js", - scriptKind: "JS" as "JS" - }; - - const host = createServerHost([]); - const projectService = createProjectService(host); - projectService.applyChangesInOpenFiles([tsFile], [], []); - const projs = projectService.synchronizeProjectList([]); - projectService.findProject(projs[0].info.projectName).getLanguageService().getNavigationBarItems(tsFile.fileName); - projectService.synchronizeProjectList([projs[0].info]); - projectService.applyChangesInOpenFiles([jsFile], [], []); - }); - - it("config file is deleted", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1;" - }; - const file2 = { - path: "/a/b/f2.ts", - content: "let y = 2;" - }; - const config = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: {} }) - }; - const host = createServerHost([file1, file2, config]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); - - projectService.openClientFile(file2.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); - - host.reloadFS([file1, file2]); - host.checkTimeoutQueueLengthAndRun(1); - checkNumberOfProjects(projectService, { inferredProjects: 2 }); - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); - checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); - }); - - it("loading files with correct priority", () => { - const f1 = { - path: "/a/main.ts", - content: "let x = 1" - }; - const f2 = { - path: "/a/main.js", - content: "var y = 1" - }; - const config = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { allowJs: true } - }) - }; - const host = createServerHost([f1, f2, config]); - const projectService = createProjectService(host); - projectService.setHostConfiguration({ - extraFileExtensions: [ - { extension: ".js", isMixedContent: false }, - { extension: ".html", isMixedContent: true } - ] - }); - projectService.openClientFile(f1.path); - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, config.path]); - - // Should close configured project with next file open - projectService.closeClientFile(f1.path); - - projectService.openClientFile(f2.path); - projectService.checkNumberOfProjects({ inferredProjects: 1 }); - assert.isUndefined(projectService.configuredProjects.get(config.path)); - checkProjectActualFiles(projectService.inferredProjects[0], [f2.path]); - }); - - it("tsconfig script block support", () => { - const file1 = { - path: "/a/b/f1.ts", - content: ` ` - }; - const file2 = { - path: "/a/b/f2.html", - content: `var hello = "hello";` - }; - const config = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: { allowJs: true } }) - }; - const host = createServerHost([file1, file2, config]); - const session = createSession(host); - openFilesForSession([file1], session); - const projectService = session.getProjectService(); - - // HTML file will not be included in any projects yet - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const configuredProj = configuredProjectAt(projectService, 0); - checkProjectActualFiles(configuredProj, [file1.path, config.path]); - - // Specify .html extension as mixed content - const extraFileExtensions = [{ extension: ".html", scriptKind: ScriptKind.JS, isMixedContent: true }]; - const configureHostRequest = makeSessionRequest(CommandNames.Configure, { extraFileExtensions }); - session.executeCommand(configureHostRequest); - - // The configured project should now be updated to include html file - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.strictEqual(configuredProjectAt(projectService, 0), configuredProj, "Same configured project should be updated"); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); - - // Open HTML file - projectService.applyChangesInOpenFiles( - /*openFiles*/[{ fileName: file2.path, hasMixedContent: true, scriptKind: ScriptKind.JS, content: `var hello = "hello";` }], - /*changedFiles*/ undefined, - /*closedFiles*/ undefined); - - // Now HTML file is included in the project - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); - - // Check identifiers defined in HTML content are available in .ts file - const project = configuredProjectAt(projectService, 0); - let completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 1, { includeExternalModuleExports: false }); - assert(completions && completions.entries[0].name === "hello", `expected entry hello to be in completion list`); - - // Close HTML file - projectService.applyChangesInOpenFiles( - /*openFiles*/ undefined, - /*changedFiles*/ undefined, - /*closedFiles*/[file2.path]); - - // HTML file is still included in project - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); - - // Check identifiers defined in HTML content are not available in .ts file - completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 5, { includeExternalModuleExports: false }); - assert(completions && completions.entries[0].name !== "hello", `unexpected hello entry in completion list`); - }); - - it("no tsconfig script block diagnostic errors", () => { - - // #1. Ensure no diagnostic errors when allowJs is true - const file1 = { - path: "/a/b/f1.ts", - content: ` ` - }; - const file2 = { - path: "/a/b/f2.html", - content: `var hello = "hello";` - }; - const config1 = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: { allowJs: true } }) - }; - - let host = createServerHost([file1, file2, config1, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); - let session = createSession(host); - - // Specify .html extension as mixed content in a configure host request - const extraFileExtensions = [{ extension: ".html", scriptKind: ScriptKind.JS, isMixedContent: true }]; - const configureHostRequest = makeSessionRequest(CommandNames.Configure, { extraFileExtensions }); - session.executeCommand(configureHostRequest); - - openFilesForSession([file1], session); - let projectService = session.getProjectService(); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - - let diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); - assert.deepEqual(diagnostics, []); - - // #2. Ensure no errors when allowJs is false - const config2 = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: { allowJs: false } }) - }; - - host = createServerHost([file1, file2, config2, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); - session = createSession(host); - - session.executeCommand(configureHostRequest); - - openFilesForSession([file1], session); - projectService = session.getProjectService(); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - - diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); - assert.deepEqual(diagnostics, []); - - // #3. Ensure no errors when compiler options aren't specified - const config3 = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({}) - }; - - host = createServerHost([file1, file2, config3, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); - session = createSession(host); - - session.executeCommand(configureHostRequest); - - openFilesForSession([file1], session); - projectService = session.getProjectService(); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - - diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); - assert.deepEqual(diagnostics, []); - - // #4. Ensure no errors when files are explicitly specified in tsconfig - const config4 = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: { allowJs: true }, files: [file1.path, file2.path] }) - }; - - host = createServerHost([file1, file2, config4, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); - session = createSession(host); - - session.executeCommand(configureHostRequest); - - openFilesForSession([file1], session); - projectService = session.getProjectService(); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - - diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); - assert.deepEqual(diagnostics, []); - - // #4. Ensure no errors when files are explicitly excluded in tsconfig - const config5 = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: { allowJs: true }, exclude: [file2.path] }) - }; - - host = createServerHost([file1, file2, config5, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); - session = createSession(host); - - session.executeCommand(configureHostRequest); - - openFilesForSession([file1], session); - projectService = session.getProjectService(); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - - diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); - assert.deepEqual(diagnostics, []); - }); - - it("project structure update is deferred if files are not added\removed", () => { - const file1 = { - path: "/a/b/f1.ts", - content: `import {x} from "./f2"` - }; - const file2 = { - path: "/a/b/f2.ts", - content: "export let x = 1" - }; - const host = createServerHost([file1, file2]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - projectService.openClientFile(file2.path); - - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - projectService.applyChangesInOpenFiles( - /*openFiles*/ undefined, - /*changedFiles*/[{ fileName: file1.path, changes: [{ span: createTextSpan(0, file1.path.length), newText: "let y = 1" }] }], - /*closedFiles*/ undefined); - - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - const changedFiles = projectService.getChangedFiles_TestOnly(); - assert(changedFiles && changedFiles.length === 1, `expected 1 changed file, got ${JSON.stringify(changedFiles && changedFiles.length || 0)}`); - - projectService.ensureInferredProjectsUpToDate_TestOnly(); - checkNumberOfProjects(projectService, { inferredProjects: 2 }); - }); - - it("files with mixed content are handled correctly", () => { - const file1 = { - path: "/a/b/f1.html", - content: `