diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 05c026c3a7..df406d10a7 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -29,15 +29,12 @@ namespace ts { abstract class TestTypingsInstaller extends server.typingsInstaller.TypingsInstaller implements server.ITypingsInstaller { protected projectService: server.ProjectService; - constructor(private readonly host: server.ServerHost) { - super(); + constructor(readonly cachePath: string, readonly installTypingHost: server.ServerHost) { + super(cachePath, ""); this.init(); } - abstract cachePath: string; safeFileList = ""; - packageNameToTypingLocation: Map = createMap(); - postInstallActions: (( map: (t: string[]) => string[]) => void)[] = []; runPostInstallActions(map: (t: string[]) => string[]) { @@ -52,7 +49,7 @@ namespace ts { } getInstallTypingHost() { - return this.host; + return this.installTypingHost; } installPackage(packageName: string) { @@ -74,7 +71,7 @@ namespace ts { } enqueueInstallTypingsRequest(project: server.Project, typingOptions: TypingOptions) { - const request = server.createInstallTypingsRequest(project, typingOptions, this.safeFileList, this.packageNameToTypingLocation, this.cachePath); + const request = server.createInstallTypingsRequest(project, typingOptions, this.cachePath); this.install(request); } } @@ -1519,9 +1516,8 @@ namespace ts { const host = createServerHost([file1, tsconfig, packageJson]); class TypingInstaller extends TestTypingsInstaller { - cachePath = "/a/data/"; constructor(host: server.ServerHost) { - super(host); + super("/a/data/", host); } }; const installer = new TypingInstaller(host); diff --git a/src/server/server.ts b/src/server/server.ts index 14ca2ca603..b69ecc50aa 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -18,10 +18,6 @@ namespace ts.server { fork(modulePath: string): NodeChildProcess; } = require("child_process"); - const os: { - homedir(): string - } = require("os"); - interface ReadLineOptions { input: NodeJS.ReadableStream; output?: NodeJS.WritableStream; @@ -167,24 +163,8 @@ namespace ts.server { class NodeTypingsInstaller implements ITypingsInstaller { private installer: NodeChildProcess; private projectService: ProjectService; - private cachePath: string; constructor(private readonly logger: server.Logger) { - let basePath: string; - switch (process.platform) { - case "win32": - basePath = process.env.LOCALAPPDATA || process.env.APPDATA || os.homedir(); - break; - case "linux": - basePath = os.homedir(); - break; - case "darwin": - basePath = combinePaths(os.homedir(), "Library/Application Support/") - break; - } - if (basePath) { - this.cachePath = combinePaths(normalizeSlashes(basePath), "Microsoft/TypeScript"); - } } attach(projectService: ProjectService) { @@ -198,13 +178,7 @@ namespace ts.server { } enqueueInstallTypingsRequest(project: Project, typingOptions: TypingOptions): void { - const request = createInstallTypingsRequest( - project, - typingOptions, - /*safeListPath*/ (combinePaths(process.cwd(), "typingSafeList.json")), // TODO: fixme - /*packageNameToTypingLocation*/ createMap(), // TODO: fixme - this.cachePath - ); + const request = createInstallTypingsRequest(project, typingOptions); if (this.logger.hasLevel(LogLevel.verbose)) { this.logger.info(`Sending request: ${JSON.stringify(request)}`); } diff --git a/src/server/types.d.ts b/src/server/types.d.ts index 7a39323546..05a8639958 100644 --- a/src/server/types.d.ts +++ b/src/server/types.d.ts @@ -7,11 +7,9 @@ declare namespace ts.server { readonly projectName: string; readonly fileNames: string[]; readonly projectRootPath: ts.Path; - readonly safeListPath: ts.Path; - readonly packageNameToTypingLocation: ts.Map; readonly typingOptions: ts.TypingOptions; readonly compilerOptions: ts.CompilerOptions; - readonly cachePath: string; + readonly cachePath?: string; } export interface CompressedData { diff --git a/src/server/typingsInstaller/nodeTypingsInstaller.ts b/src/server/typingsInstaller/nodeTypingsInstaller.ts index fe1f372fa7..cb4b522f62 100644 --- a/src/server/typingsInstaller/nodeTypingsInstaller.ts +++ b/src/server/typingsInstaller/nodeTypingsInstaller.ts @@ -2,13 +2,39 @@ /// namespace ts.server.typingsInstaller { + + const os: { + homedir(): string + } = require("os"); + + function getGlobalCacheLocation() { + let basePath: string; + switch (process.platform) { + case "win32": + basePath = process.env.LOCALAPPDATA || process.env.APPDATA || os.homedir(); + break; + case "linux": + basePath = os.homedir(); + break; + case "darwin": + basePath = combinePaths(os.homedir(), "Library/Application Support/") + break; + } + + Debug.assert(basePath !== undefined); + return combinePaths(normalizeSlashes(basePath), "Microsoft/TypeScript"); + } + export class NodeTypingsInstaller extends TypingsInstaller { private execSync: { (command: string, options: { stdio: "ignore" }): any }; private exec: { (command: string, options: { cwd: string }, callback?: (error: Error, stdout: string, stderr: string) => void): any }; + readonly installTypingHost: InstallTypingHost = sys; + constructor() { - super(); - this.execSync = require("child_process").execSync; - this.exec = require("child_process").exec; + super(getGlobalCacheLocation(), toPath("typingSafeList.json", __dirname, createGetCanonicalFileName(sys.useCaseSensitiveFileNames))); + const { exec, execSync } = require("child_process"); + this.execSync = execSync; + this.exec = exec; } init() { @@ -38,10 +64,6 @@ namespace ts.server.typingsInstaller { } } - protected getInstallTypingHost() { - return sys; - } - protected sendResponse(response: InstallTypingsResponse) { process.send(response); } diff --git a/src/server/typingsInstaller/typingsInstaller.ts b/src/server/typingsInstaller/typingsInstaller.ts index 73735d0741..d611cc374c 100644 --- a/src/server/typingsInstaller/typingsInstaller.ts +++ b/src/server/typingsInstaller/typingsInstaller.ts @@ -9,16 +9,38 @@ namespace ts.server.typingsInstaller { path: "typings" }, /*replacer*/undefined, /*space*/4); + interface TsdConfig { + installed: MapLike; + } + + 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; + } + export abstract class TypingsInstaller { private isTsdInstalled: boolean; - private missingTypings: Map = createMap(); + + private packageNameToTypingLocation: Map = createMap(); + private missingTypingsSet: Map = createMap(); + private knownCachesSet: Map = createMap(); + + abstract readonly installTypingHost: InstallTypingHost; + + constructor(readonly globalCachePath: string, readonly safeListPath: Path) { + } init() { this.isTsdInstalled = this.isPackageInstalled("tsd"); if (!this.isTsdInstalled) { this.isTsdInstalled = this.installPackage("tsd"); } + this.processCacheLocation(this.globalCachePath); } install(req: InstallTypingsRequest) { @@ -26,43 +48,91 @@ namespace ts.server.typingsInstaller { return; } + // load existing typing information from the cache + if (req.cachePath) { + this.processCacheLocation(req.cachePath); + } + const discoverTypingsResult = JsTyping.discoverTypings( - this.getInstallTypingHost(), + this.installTypingHost, req.fileNames, req.projectRootPath, - req.safeListPath, - req.packageNameToTypingLocation, + this.safeListPath, + this.packageNameToTypingLocation, req.typingOptions, req.compilerOptions); // respond with whatever cached typings we have now this.sendResponse(this.createResponse(req, discoverTypingsResult.cachedTypingPaths)); + // start watching files this.watchFiles(discoverTypingsResult.filesToWatch); + // install typings and this.installTypings(req, discoverTypingsResult.cachedTypingPaths, discoverTypingsResult.newTypingNames); } + private processCacheLocation(cacheLocation: string) { + if (this.knownCachesSet[cacheLocation]) { + return; + } + const tsdJson = combinePaths(cacheLocation, "tsd.json"); + if (this.installTypingHost.fileExists(tsdJson)) { + const tsdConfig = JSON.parse(this.installTypingHost.readFile(tsdJson)); + if (tsdConfig.installed) { + for (const key in tsdConfig.installed) { + // key is / + const packageName = getPackageName(key); + if (!packageName) { + continue; + } + const typingFile = tsdTypingToFileName(cacheLocation, key); + const existingTypingFile = this.packageNameToTypingLocation[packageName]; + if (existingTypingFile === typingFile) { + continue; + } + if (existingTypingFile) { + // TODO: log warning + } + this.packageNameToTypingLocation[packageName] = typingFile; + } + } + } + this.knownCachesSet[cacheLocation] = true; + } + private installTypings(req: InstallTypingsRequest, currentlyCachedTypings: string[], typingsToInstall: string[]) { - typingsToInstall = filter(typingsToInstall, x => !hasProperty(this.missingTypings, x)); + typingsToInstall = filter(typingsToInstall, x => !this.missingTypingsSet[x]); if (typingsToInstall.length === 0) { return; } // TODO: install typings and send response when they are ready - const host = this.getInstallTypingHost(); const tsdPath = combinePaths(req.cachePath, "tsd.json"); - if (!host.fileExists(tsdPath)) { - this.ensureDirectoryExists(req.cachePath, host); - host.writeFile(tsdPath, DefaultTsdSettings); + if (!this.installTypingHost.fileExists(tsdPath)) { + this.ensureDirectoryExists(req.cachePath, this.installTypingHost); + this.installTypingHost.writeFile(tsdPath, DefaultTsdSettings); } this.runTsd(req.cachePath, typingsToInstall, installedTypings => { - // TODO: record new missing package names // TODO: watch project directory - const typingDirectory = combinePaths(req.cachePath, "typings"); - installedTypings = installedTypings.map(x => combinePaths(typingDirectory, x)); - this.sendResponse(this.createResponse(req, currentlyCachedTypings.concat(installedTypings))); + const installedPackages: Map = createMap(); + const installedTypingFiles: string[] = []; + for (const t of installedTypings) { + const packageName = getPackageName(t); + if (!packageName) { + continue; + } + installedPackages[packageName] = true; + installedTypingFiles.push(tsdTypingToFileName(req.cachePath, t)); + } + for (const toInstall of typingsToInstall) { + if (!installedPackages[toInstall]) { + this.missingTypingsSet[toInstall] = true; + } + } + + this.sendResponse(this.createResponse(req, currentlyCachedTypings.concat(installedTypingFiles))); }); } @@ -91,7 +161,6 @@ namespace ts.server.typingsInstaller { protected abstract isPackageInstalled(packageName: string): boolean; protected abstract installPackage(packageName: string): boolean; - protected abstract getInstallTypingHost(): InstallTypingHost; protected abstract sendResponse(response: InstallTypingsResponse): void; protected abstract runTsd(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void): void; } diff --git a/src/server/utilities.ts b/src/server/utilities.ts index e3550a693f..9fa526ea99 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -1,4 +1,3 @@ -/// /// namespace ts.server { @@ -30,15 +29,13 @@ namespace ts.server { export type Types = Err | Info | Perf; } - export function createInstallTypingsRequest(project: Project, typingOptions: TypingOptions, safeListPath: Path, packageNameToTypingLocation: Map, cachePath: string): InstallTypingsRequest { + export function createInstallTypingsRequest(project: Project, typingOptions: TypingOptions, cachePath?: string): InstallTypingsRequest { return { projectName: project.getProjectName(), fileNames: project.getFileNames(), compilerOptions: project.getCompilerOptions(), typingOptions, projectRootPath: (project.projectKind === ProjectKind.Inferred ? "" : getDirectoryPath(project.getProjectName())), // TODO: fixme - safeListPath, - packageNameToTypingLocation, cachePath }; }