diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index d38ce359e8..a209db4e73 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -18627,6 +18627,9 @@ namespace ts { continue; } const file = host.getSourceFile(resolvedDirective.resolvedFileName); + if (!file) { + continue; + } fileToDirective.set(file.path, key); } } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 8cbc3848cf..e22f1752ce 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -2,6 +2,17 @@ /// namespace ts.projectSystem { + const safeList = { + path: "/safeList.json", + content: JSON.stringify({ + commander: "commander", + express: "express", + jquery: "jquery", + lodash: "lodash", + moment: "moment" + }) + }; + export function notImplemented(): any { throw new Error("Not yet implemented"); } @@ -31,11 +42,11 @@ namespace ts.projectSystem { export class TestTypingsInstaller extends server.typingsInstaller.TypingsInstaller implements server.ITypingsInstaller { protected projectService: server.ProjectService; constructor(readonly globalTypingsCacheLocation: string, readonly installTypingHost: server.ServerHost) { - super(globalTypingsCacheLocation, ""); + super(globalTypingsCacheLocation, safeList.path); this.init(); } - safeFileList = ""; + safeFileList = safeList.path; postInstallActions: ((map: (t: string[]) => string[]) => void)[] = []; runPostInstallActions(map: (t: string[]) => string[]) { @@ -56,15 +67,11 @@ namespace ts.projectSystem { return this.installTypingHost; } - installPackage(packageName: string) { - return true; - } - isPackageInstalled(packageName: string) { return true; } - runTsd(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void) { + runInstall(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void) { this.postInstallActions.push(map => { postInstallAction(map(typingsToInstall)); }); @@ -106,11 +113,13 @@ namespace ts.projectSystem { if (!params) { params = {}; } - return new TestServerHost( + const host = new TestServerHost( params.useCaseSensitiveFileNames !== undefined ? params.useCaseSensitiveFileNames : false, params.executingFilePath || getExecutingFilePathFromLibFile(libFilePath), params.currentDirectory || "/", fileOrFolderList); + host.createFileOrFolder(safeList, /*createParentDirectory*/ true); + return host; } export function createSession(host: server.ServerHost, typingsInstaller?: server.ITypingsInstaller) { diff --git a/src/harness/unittests/typingsInstaller.ts b/src/harness/unittests/typingsInstaller.ts index 18c1e89ec7..47373bfd26 100644 --- a/src/harness/unittests/typingsInstaller.ts +++ b/src/harness/unittests/typingsInstaller.ts @@ -4,7 +4,7 @@ namespace ts.projectSystem { describe("typings installer", () => { - it("configured projects (tsd installed) 1", () => { + it("configured projects (typings installed) 1", () => { const file1 = { path: "/a/b/app.js", content: "" @@ -31,7 +31,7 @@ namespace ts.projectSystem { }; const jquery = { - path: "/a/data/typings/jquery/jquery.d.ts", + path: "/a/data/node_modules/@types/jquery/index.d.ts", content: "declare const $: { x: number }" }; @@ -44,18 +44,18 @@ namespace ts.projectSystem { const p = projectService.configuredProjects[0]; checkProjectActualFiles(p, [file1.path]); - assert(host.fileExists(combinePaths(installer.globalTypingsCacheLocation, "tsd.json"))); + assert(host.fileExists(combinePaths(installer.globalTypingsCacheLocation, "package.json"))); installer.runPostInstallActions(t => { assert.deepEqual(t, ["jquery"]); host.createFileOrFolder(jquery, /*createParentDirectory*/ true); - return ["jquery/jquery.d.ts"]; + return ["@types/jquery"]; }); checkNumberOfProjects(projectService, { configuredProjects: 1 }); checkProjectActualFiles(p, [file1.path, jquery.path]); }); - it("inferred project (tsd installed)", () => { + it("inferred project (typings installed)", () => { const file1 = { path: "/a/b/app.js", content: "" @@ -71,7 +71,7 @@ namespace ts.projectSystem { }; const jquery = { - path: "/a/data/typings/jquery/jquery.d.ts", + path: "/a/data/node_modules/@types/jquery/index.d.ts", content: "declare const $: { x: number }" }; const host = createServerHost([file1, packageJson]); @@ -84,12 +84,12 @@ namespace ts.projectSystem { const p = projectService.inferredProjects[0]; checkProjectActualFiles(p, [file1.path]); - assert(host.fileExists(combinePaths(installer.globalTypingsCacheLocation, "tsd.json"))); + assert(host.fileExists(combinePaths(installer.globalTypingsCacheLocation, "package.json"))); installer.runPostInstallActions(t => { assert.deepEqual(t, ["jquery"]); host.createFileOrFolder(jquery, /*createParentDirectory*/ true); - return ["jquery/jquery.d.ts"]; + return ["@types/jquery"]; }); checkNumberOfProjects(projectService, { inferredProjects: 1 }); checkProjectActualFiles(p, [file1.path, jquery.path]); @@ -157,7 +157,7 @@ namespace ts.projectSystem { }; const host = createServerHost([file1]); let enqueueIsCalled = false; - let runTsdIsCalled = false; + let runInstallIsCalled = false; const installer = new (class extends TestTypingsInstaller { constructor() { super("", host); @@ -166,10 +166,10 @@ namespace ts.projectSystem { enqueueIsCalled = true; super.enqueueInstallTypingsRequest(project, typingOptions); } - runTsd(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void): void { + runInstall(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void): void { assert.deepEqual(typingsToInstall, ["node"]); - runTsdIsCalled = true; - super.runTsd(cachePath, typingsToInstall, postInstallAction); + runInstallIsCalled = true; + super.runInstall(cachePath, typingsToInstall, postInstallAction); } })(); @@ -184,7 +184,188 @@ namespace ts.projectSystem { // 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"); + assert.isTrue(runInstallIsCalled, "expected 'runInstallIsCalled' to be true"); + }); + + it("external project - no typing options, with only js, jsx, d.ts files", () => { + // Tests: + // 1. react typings are installed for .jsx + // 2. loose files names are matched against safe list for typings if + // this is a JS project (only js, jsx, d.ts files are present) + const file1 = { + path: "/a/b/lodash.js", + content: "" + }; + const file2 = { + path: "/a/b/file2.jsx", + content: "" + }; + const file3 = { + path: "/a/b/file3.d.ts", + content: "" + }; + const react = { + path: "/a/data/node_modules/@types/react/index.d.ts", + content: "declare const react: { x: number }" + }; + const lodash = { + path: "/a/data/node_modules/@types/lodash/index.d.ts", + content: "declare const lodash: { x: number }" + }; + + const host = createServerHost([file1, file2, file3]); + const installer = new TestTypingsInstaller("/a/data/", host); + + const projectFileName = "/a/app/test.csproj"; + const projectService = createProjectService(host, { typingsInstaller: installer }); + projectService.openExternalProject({ + projectFileName, + options: { allowJS: true, moduleResolution: ModuleResolutionKind.NodeJs }, + rootFiles: [toExternalFile(file1.path), toExternalFile(file2.path), toExternalFile(file3.path)], + typingOptions: {} + }); + + const p = projectService.externalProjects[0]; + projectService.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(p, [file1.path, file2.path, file3.path]); + + installer.runPostInstallActions(t => { + assert.deepEqual(t, ["lodash", "react"]); + host.createFileOrFolder(lodash, /*createParentDirectory*/ true); + host.createFileOrFolder(react, /*createParentDirectory*/ true); + return ["@types/lodash", "@types/react"]; + }); + + checkNumberOfProjects(projectService, { externalProjects: 1 }); + checkProjectActualFiles(p, [file1.path, file2.path, file3.path, lodash.path, react.path]); + }); + + it("external project - no typing options, with js & ts files", () => { + // Tests: + // 1. No typings are included for JS projects when the project contains ts files + const file1 = { + path: "/a/b/jquery.js", + content: "" + }; + const file2 = { + path: "/a/b/file2.ts", + content: "" + }; + + const host = createServerHost([file1, file2]); + let enqueueIsCalled = false; + let runInstallIsCalled = false; + let runPostInstallIsCalled = false; + const installer = new (class extends TestTypingsInstaller { + constructor() { + super("/a/data/", host); + } + enqueueInstallTypingsRequest(project: server.Project, typingOptions: TypingOptions) { + enqueueIsCalled = true; + super.enqueueInstallTypingsRequest(project, typingOptions); + } + runInstall(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void): void { + runInstallIsCalled = true; + super.runInstall(cachePath, typingsToInstall, postInstallAction); + } + })(); + + const projectFileName = "/a/app/test.csproj"; + const projectService = createProjectService(host, { typingsInstaller: installer }); + projectService.openExternalProject({ + projectFileName, + options: { allowJS: true, moduleResolution: ModuleResolutionKind.NodeJs }, + rootFiles: [toExternalFile(file1.path), toExternalFile(file2.path)], + typingOptions: {} + }); + + const p = projectService.externalProjects[0]; + projectService.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(p, [file1.path, file2.path]); + + installer.runPostInstallActions(t => { + runPostInstallIsCalled = true; + return []; + }); + + checkNumberOfProjects(projectService, { externalProjects: 1 }); + checkProjectActualFiles(p, [file1.path, file2.path]); + assert.isFalse(enqueueIsCalled, "expected 'enqueueIsCalled' to be false"); + assert.isFalse(runInstallIsCalled, "expected 'runInstallIsCalled' to be false"); + assert.isFalse(runPostInstallIsCalled, "expected 'runPostInstallIsCalled' to be false"); + }); + + it("external project - with typing options, with only js, d.ts files", () => { + // Tests: + // 1. Safelist matching, typing options includes/excludes and package.json typings are all acquired + // 2. Types for safelist matches are not included when they also appear in the typing option exclude list + // 3. Multiple includes and excludes are respected in typing options + const file1 = { + path: "/a/b/lodash.js", + content: "" + }; + const file2 = { + path: "/a/b/commander.js", + content: "" + }; + const file3 = { + path: "/a/b/file3.d.ts", + content: "" + }; + const packageJson = { + path: "/a/b/package.json", + content: JSON.stringify({ + name: "test", + dependencies: { + express: "^3.1.0" + } + }) + }; + + const commander = { + path: "/a/data/node_modules/@types/commander/index.d.ts", + content: "declare const commander: { x: number }" + }; + const express = { + path: "/a/data/node_modules/@types/express/index.d.ts", + content: "declare const express: { x: number }" + }; + const jquery = { + path: "/a/data/node_modules/@types/jquery/index.d.ts", + content: "declare const jquery: { x: number }" + }; + const moment = { + path: "/a/data/node_modules/@types/moment/index.d.ts", + content: "declare const moment: { x: number }" + }; + + const host = createServerHost([file1, file2, file3, packageJson]); + const installer = new TestTypingsInstaller("/a/data/", host); + + const projectFileName = "/a/app/test.csproj"; + const projectService = createProjectService(host, { typingsInstaller: installer }); + projectService.openExternalProject({ + projectFileName, + options: { allowJS: true, moduleResolution: ModuleResolutionKind.NodeJs }, + rootFiles: [toExternalFile(file1.path), toExternalFile(file2.path), toExternalFile(file3.path)], + typingOptions: { include: ["jquery", "moment"], exclude: ["lodash"] } + }); + + const p = projectService.externalProjects[0]; + projectService.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(p, [file1.path, file2.path, file3.path]); + + installer.runPostInstallActions(t => { + assert.deepEqual(t, ["jquery", "moment", "express", "commander" ]); + host.createFileOrFolder(commander, /*createParentDirectory*/ true); + host.createFileOrFolder(express, /*createParentDirectory*/ true); + host.createFileOrFolder(jquery, /*createParentDirectory*/ true); + host.createFileOrFolder(moment, /*createParentDirectory*/ true); + return ["@types/commander", "@types/express", "@types/jquery", "@types/moment"]; + }); + + checkNumberOfProjects(projectService, { externalProjects: 1 }); + checkProjectActualFiles(p, [file1.path, file2.path, file3.path, commander.path, express.path, jquery.path, moment.path]); }); }); } \ No newline at end of file diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index bd46f26923..a840a73b73 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -312,6 +312,7 @@ namespace ts.server { const info = this.getScriptInfoForNormalizedPath(fileName); if (!info) { this.logger.info(`Error: got watch notification for unknown file: ${fileName}`); + return; } if (!this.host.fileExists(fileName)) { diff --git a/src/server/project.ts b/src/server/project.ts index bb4808a767..7708cf797f 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -301,7 +301,7 @@ namespace ts.server { return true; } let hasChanges = this.updateGraphWorker(); - const cachedTypings = this.projectService.typingsCache.getTypingsForProject(this); + const cachedTypings = this.projectService.typingsCache.getTypingsForProject(this, hasChanges); if (this.setTypings(cachedTypings)) { hasChanges = this.updateGraphWorker() || hasChanges; } @@ -423,17 +423,15 @@ namespace ts.server { const added: string[] = []; const removed: string[] = []; for (const id in currentFiles) { - if (hasProperty(currentFiles, id) && !hasProperty(lastReportedFileNames, id)) { + if (!hasProperty(lastReportedFileNames, id)) { added.push(id); } } for (const id in lastReportedFileNames) { - if (hasProperty(lastReportedFileNames, id) && !hasProperty(currentFiles, id)) { + if (!hasProperty(currentFiles, id)) { removed.push(id); } } - this.lastReportedFileNames = currentFiles; - this.lastReportedFileNames = currentFiles; this.lastReportedVersion = this.projectStructureVersion; return { info, changes: { added, removed }, projectErrors: this.projectErrors }; diff --git a/src/server/session.ts b/src/server/session.ts index 1f55673aef..fc6b4669b1 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -168,7 +168,7 @@ namespace ts.server { : undefined; this.projectService = new ProjectService(host, logger, cancellationToken, useSingleInferredProject, typingsInstaller, eventHandler); - this.gcTimer = new GcTimer(host, /*delay*/ 15000, logger); + this.gcTimer = new GcTimer(host, /*delay*/ 7000, logger); } private handleEvent(event: ProjectServiceEvent) { diff --git a/src/server/typingsCache.ts b/src/server/typingsCache.ts index 25efa02c34..889cfe1fb8 100644 --- a/src/server/typingsCache.ts +++ b/src/server/typingsCache.ts @@ -76,7 +76,7 @@ namespace ts.server { constructor(private readonly installer: ITypingsInstaller) { } - getTypingsForProject(project: Project): TypingsArray { + getTypingsForProject(project: Project, forceRefresh: boolean): TypingsArray { const typingOptions = project.getTypingOptions(); if (!typingOptions || !typingOptions.enableAutoDiscovery) { @@ -85,9 +85,7 @@ namespace ts.server { const entry = this.perProjectCache[project.getProjectName()]; const result: TypingsArray = entry ? entry.typings : emptyArray; - if (!entry || typingOptionsChanged(typingOptions, entry.typingOptions) || compilerOptionsChanged(project.getCompilerOptions(), entry.compilerOptions)) { - // something has been changed, issue a request to update typings - this.installer.enqueueInstallTypingsRequest(project, typingOptions); + if (forceRefresh || !entry || typingOptionsChanged(typingOptions, entry.typingOptions) || compilerOptionsChanged(project.getCompilerOptions(), entry.compilerOptions)) { // Note: entry is now poisoned since it does not really contain typings for a given combination of compiler options\typings options. // instead it acts as a placeholder to prevent issuing multiple requests this.perProjectCache[project.getProjectName()] = { @@ -96,6 +94,8 @@ namespace ts.server { typings: result, poisoned: true }; + // something has been changed, issue a request to update typings + this.installer.enqueueInstallTypingsRequest(project, typingOptions); } return result; } diff --git a/src/server/typingsInstaller/nodeTypingsInstaller.ts b/src/server/typingsInstaller/nodeTypingsInstaller.ts index 97228e7375..0d22b1b364 100644 --- a/src/server/typingsInstaller/nodeTypingsInstaller.ts +++ b/src/server/typingsInstaller/nodeTypingsInstaller.ts @@ -24,7 +24,7 @@ namespace ts.server.typingsInstaller { private exec: { (command: string, options: { cwd: string }, callback?: (error: Error, stdout: string, stderr: string) => void): any }; private npmBinPath: string; - private tsdRunCount = 1; + private installRunCount = 1; readonly installTypingHost: InstallTypingHost = sys; constructor(globalTypingsCacheLocation: string, log: Log) { @@ -79,23 +79,6 @@ namespace ts.server.typingsInstaller { } } - protected installPackage(packageName: string) { - try { - const output = this.execSync(`npm install --silent --global ${packageName}`, { stdio: "pipe" }).toString(); - if (this.log.isEnabled()) { - this.log.writeLine(`installPackage::stdout '${output}'`); - } - return true; - } - catch (e) { - if (this.log.isEnabled()) { - this.log.writeLine(`installPackage::err::stdout '${e.stdout && e.stdout.toString()}'`); - this.log.writeLine(`installPackage::err::stderr '${e.stdout && e.stderr.toString()}'`); - } - return false; - } - } - protected sendResponse(response: SetTypings | InvalidateCachedTypings) { if (this.log.isEnabled()) { this.log.writeLine(`Sending response: ${JSON.stringify(response)}`); @@ -106,32 +89,61 @@ namespace ts.server.typingsInstaller { } } - protected runTsd(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void): void { - const id = this.tsdRunCount; - this.tsdRunCount++; - const tsdPath = combinePaths(this.npmBinPath, "tsd"); - const command = `${tsdPath} install ${typingsToInstall.join(" ")} -ros`; - if (this.log.isEnabled()) { - this.log.writeLine(`Running tsd ${id}, command '${command}'. cache path '${cachePath}'`); + protected runInstall(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void): void { + const id = this.installRunCount; + this.installRunCount++; + let execInstallCmdCount = 0; + const filteredTypings: string[] = []; + for (const typing of typingsToInstall) { + const command = `npm view @types/${typing} --silent name`; + this.execAsync("npm view", command, cachePath, id, (err, stdout, stderr) => { + if (stdout) { + filteredTypings.push(typing); + } + execInstallCmdCount++; + if (execInstallCmdCount === typingsToInstall.length) { + installFilteredTypings(this, filteredTypings); + } + }); } - this.exec(command, { cwd: cachePath }, (err, stdout, stderr) => { - if (this.log.isEnabled()) { - this.log.writeLine(`TSD ${id} stdout: ${stdout}`); - this.log.writeLine(`TSD ${id} stderr: ${stderr}`); - } - const i = stdout.indexOf("running install"); - if (i < 0) { - return; - } - const installedTypings: string[] = []; - const expr = /^\s*-\s*(\S+)\s*$/gm; - expr.lastIndex = i; - let match: RegExpExecArray; - while (match = expr.exec(stdout)) { - installedTypings.push(match[1]); + function installFilteredTypings(self: NodeTypingsInstaller, filteredTypings: string[]) { + const command = `npm install ${filteredTypings.map(t => "@types/" + t).join(" ")} --save-dev`; + self.execAsync("npm install", command, cachePath, id, (err, stdout, stderr) => { + if (stdout) { + reportInstalledTypings(self); + } + }); + } + + function reportInstalledTypings(self: NodeTypingsInstaller) { + const command = "npm ls -json"; + self.execAsync("npm ls", command, cachePath, id, (err, stdout, stderr) => { + let installedTypings: string[]; + try { + const response = JSON.parse(stdout); + if (response.dependencies) { + installedTypings = getOwnKeys(response.dependencies); + } + } + catch (e) { + self.log.writeLine(`Error parsing installed @types dependencies. Error details: ${e.message}`); + } + postInstallAction(installedTypings || []); + }); + } + } + + private execAsync(prefix: string, command: string, cwd: string, requestId: number, cb: (err: Error, stdout: string, stderr: string) => void) { + if (this.log.isEnabled()) { + this.log.writeLine(`#${requestId} running command '${command}'.`); + } + this.exec(command, { cwd }, (err, stdout, stderr) => { + if (this.log.isEnabled()) { + this.log.writeLine(`${prefix} #${requestId} stdout: ${stdout}`); + this.log.writeLine(`${prefix} #${requestId} stderr: ${stderr}`); } - postInstallAction(installedTypings); + cb(err, stdout, stderr); }); } } diff --git a/src/server/typingsInstaller/typingsInstaller.ts b/src/server/typingsInstaller/typingsInstaller.ts index f4a899068f..c96fe3c6c4 100644 --- a/src/server/typingsInstaller/typingsInstaller.ts +++ b/src/server/typingsInstaller/typingsInstaller.ts @@ -1,16 +1,10 @@ +/// /// /// namespace ts.server.typingsInstaller { - const DefaultTsdSettings = JSON.stringify({ - version: "v4", - repo: "DefinitelyTyped/DefinitelyTyped", - ref: "master", - path: "typings" - }, /*replacer*/undefined, /*space*/4); - - interface TsdConfig { - installed: MapLike; + interface NpmConfig { + devDependencies: MapLike; } export interface Log { @@ -23,22 +17,15 @@ namespace ts.server.typingsInstaller { writeLine: () => {} }; - function tsdTypingToFileName(cachePath: string, tsdTypingFile: string) { - return combinePaths(cachePath, `typings/${tsdTypingFile}`); - } - - function getPackageName(tsdTypingFile: string) { - const idx = tsdTypingFile.indexOf("/"); - return idx > 0 ? tsdTypingFile.substr(0, idx) : undefined; + function typingToFileName(cachePath: string, packageName: string, installTypingHost: InstallTypingHost): string { + const result = resolveModuleName(packageName, combinePaths(cachePath, "index.d.ts"), { moduleResolution: ModuleResolutionKind.NodeJs }, installTypingHost); + return result.resolvedModule && result.resolvedModule.resolvedFileName; } export abstract class TypingsInstaller { - private isTsdInstalled: boolean; - private packageNameToTypingLocation: Map = createMap(); private missingTypingsSet: Map = createMap(); private knownCachesSet: Map = createMap(); - private projectWatchers: Map = createMap(); abstract readonly installTypingHost: InstallTypingHost; @@ -50,20 +37,6 @@ namespace ts.server.typingsInstaller { } init() { - this.isTsdInstalled = this.isPackageInstalled("tsd"); - if (this.log.isEnabled()) { - this.log.writeLine(`isTsdInstalled: ${this.isTsdInstalled}`); - } - - if (!this.isTsdInstalled) { - if (this.log.isEnabled()) { - this.log.writeLine(`tsd is not installed, installing tsd...`); - } - this.isTsdInstalled = this.installPackage("tsd"); - if (this.log.isEnabled()) { - this.log.writeLine(`isTsdInstalled: ${this.isTsdInstalled}`); - } - } this.processCacheLocation(this.globalCachePath); } @@ -94,13 +67,6 @@ namespace ts.server.typingsInstaller { } install(req: DiscoverTypings) { - if (!this.isTsdInstalled) { - if (this.log.isEnabled()) { - this.log.writeLine(`tsd is not installed, ignoring request...`); - } - return; - } - if (this.log.isEnabled()) { this.log.writeLine(`Got install request ${JSON.stringify(req)}`); } @@ -153,23 +119,26 @@ namespace ts.server.typingsInstaller { } return; } - const tsdJson = combinePaths(cacheLocation, "tsd.json"); + const packageJson = combinePaths(cacheLocation, "package.json"); if (this.log.isEnabled()) { - this.log.writeLine(`Trying to find '${tsdJson}'...`); + this.log.writeLine(`Trying to find '${packageJson}'...`); } - if (this.installTypingHost.fileExists(tsdJson)) { - const tsdConfig = JSON.parse(this.installTypingHost.readFile(tsdJson)); + if (this.installTypingHost.fileExists(packageJson)) { + const npmConfig = JSON.parse(this.installTypingHost.readFile(packageJson)); if (this.log.isEnabled()) { - this.log.writeLine(`Loaded content of '${tsdJson}': ${JSON.stringify(tsdConfig)}`); + this.log.writeLine(`Loaded content of '${npmConfig}': ${JSON.stringify(npmConfig)}`); } - if (tsdConfig.installed) { - for (const key in tsdConfig.installed) { - // key is / - const packageName = getPackageName(key); + if (npmConfig.devDependencies) { + for (const key in npmConfig.devDependencies) { + // key is @types/ + const packageName = getBaseFileName(key); if (!packageName) { continue; } - const typingFile = tsdTypingToFileName(cacheLocation, key); + const typingFile = typingToFileName(cacheLocation, packageName, this.installTypingHost); + if (!typingFile) { + continue; + } const existingTypingFile = this.packageNameToTypingLocation[packageName]; if (existingTypingFile === typingFile) { continue; @@ -204,20 +173,19 @@ namespace ts.server.typingsInstaller { return; } - // TODO: install typings and send response when they are ready - const tsdPath = combinePaths(cachePath, "tsd.json"); + const npmConfigPath = combinePaths(cachePath, "package.json"); if (this.log.isEnabled()) { - this.log.writeLine(`Tsd config file: ${tsdPath}`); + this.log.writeLine(`Npm config file: ${npmConfigPath}`); } - if (!this.installTypingHost.fileExists(tsdPath)) { + if (!this.installTypingHost.fileExists(npmConfigPath)) { if (this.log.isEnabled()) { - this.log.writeLine(`Tsd config file '${tsdPath}' is missing, creating new one...`); + this.log.writeLine(`Npm config file: '${npmConfigPath}' is missing, creating new one...`); } this.ensureDirectoryExists(cachePath, this.installTypingHost); - this.installTypingHost.writeFile(tsdPath, DefaultTsdSettings); + this.installTypingHost.writeFile(npmConfigPath, "{}"); } - this.runTsd(cachePath, typingsToInstall, installedTypings => { + this.runInstall(cachePath, typingsToInstall, installedTypings => { // TODO: watch project directory if (this.log.isEnabled()) { this.log.writeLine(`Requested to install typings ${JSON.stringify(typingsToInstall)}, installed typings ${JSON.stringify(installedTypings)}`); @@ -225,12 +193,19 @@ namespace ts.server.typingsInstaller { const installedPackages: Map = createMap(); const installedTypingFiles: string[] = []; for (const t of installedTypings) { - const packageName = getPackageName(t); + const packageName = getBaseFileName(t); if (!packageName) { continue; } installedPackages[packageName] = true; - installedTypingFiles.push(tsdTypingToFileName(cachePath, t)); + const typingFile = typingToFileName(cachePath, packageName, this.installTypingHost); + if (!typingFile) { + continue; + } + if (!this.packageNameToTypingLocation[packageName]) { + this.packageNameToTypingLocation[packageName] = typingFile; + } + installedTypingFiles.push(typingFile); } if (this.log.isEnabled()) { this.log.writeLine(`Installed typing files ${JSON.stringify(installedTypingFiles)}`); @@ -292,8 +267,7 @@ namespace ts.server.typingsInstaller { } protected abstract isPackageInstalled(packageName: string): boolean; - protected abstract installPackage(packageName: string): boolean; protected abstract sendResponse(response: SetTypings | InvalidateCachedTypings): void; - protected abstract runTsd(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void): void; + protected abstract runInstall(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void): void; } } \ No newline at end of file