diff --git a/Jakefile.js b/Jakefile.js index f5012a9a7a4..7c620db00f4 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -186,6 +186,7 @@ var harnessSources = harnessCoreSources.concat([ "scriptInfo.ts", "lsHost.ts", "project.ts", + "typingsCache.ts", "editorServices.ts", "protocol.d.ts", "session.ts", diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index f61d7d6b497..ec68e423235 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -6,6 +6,7 @@ /// /// /// +/// namespace ts.server { export const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024; @@ -130,6 +131,9 @@ namespace ts.server { } export class ProjectService { + + public readonly typingsCache: TypingsCache; + private readonly documentRegistry: DocumentRegistry; /** @@ -654,6 +658,7 @@ namespace ts.server { compilerOptions: parsedCommandLine.options, configHasFilesProperty: configObj.config["files"] !== undefined, wildcardDirectories: parsedCommandLine.wildcardDirectories, + typingOptions: parsedCommandLine.typingOptions }; return { success: true, projectOptions }; } @@ -697,6 +702,7 @@ namespace ts.server { this.documentRegistry, projectOptions.configHasFilesProperty, projectOptions.compilerOptions, + projectOptions.typingOptions, projectOptions.wildcardDirectories, /*languageServiceEnabled*/ !sizeLimitExceeded); diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index 36bdc7a9afe..e24d8f504f2 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -101,7 +101,7 @@ namespace ts.server { } getScriptFileNames() { - return this.project.getRootFiles(); + return this.project.getRootFilesLSHost(); } getScriptKind(fileName: string) { diff --git a/src/server/project.ts b/src/server/project.ts index cb1243c979b..ee08810ae82 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -2,6 +2,7 @@ /// /// /// +/// namespace ts.server { @@ -25,7 +26,6 @@ namespace ts.server { private program: ts.Program; private languageService: LanguageService; - /** * Set of files that was returned from the last call to getChangesSinceVersion. */ @@ -47,6 +47,8 @@ namespace ts.server { */ private projectStateVersion = 0; + private typingFiles: string[]; + constructor( readonly projectKind: ProjectKind, readonly projectService: ProjectService, @@ -74,7 +76,7 @@ namespace ts.server { this.markAsDirty(); } - getLanguageService(ensureSynchronized = true): LanguageService { + getLanguageService(ensureSynchronized = true): LanguageService { if (ensureSynchronized) { this.updateGraph(); } @@ -136,6 +138,21 @@ namespace ts.server { return this.rootFiles && this.rootFiles.map(info => info.fileName); } + getRootFilesLSHost() { + const result: string[] = []; + if (this.rootFiles) { + for (const f of this.rootFiles) { + result.push(f.fileName); + } + if (this.typingFiles) { + for (const f of this.typingFiles) { + result.push(f); + } + } + } + return result; + } + getRootScriptInfos() { return this.rootFiles; } @@ -209,16 +226,35 @@ namespace ts.server { if (!this.languageServiceEnabled) { return true; } + const hasChanges = this.updateGraphWorker(); + if (hasChanges) { + if (this.setTypings(this.projectService.typingsCache.getTypingsForProject(this))) { + this.updateGraphWorker(); + } + this.projectStructureVersion++; + } + return hasChanges; + } + setTypings(typings: string[]): boolean { + if (typings === this.typingFiles) { + return false; + } + this.typingFiles = typings; + this.markAsDirty(); + return true; + } + + private updateGraphWorker() { const oldProgram = this.program; this.program = this.languageService.getProgram(); - const oldProjectStructureVersion = this.projectStructureVersion; + let hasChanges = false; // bump up the version if // - oldProgram is not set - this is a first time updateGraph is called // - newProgram is different from the old program and structure of the old program was not reused. if (!oldProgram || (this.program !== oldProgram && !oldProgram.structureIsReused)) { - this.projectStructureVersion++; + hasChanges = true; if (oldProgram) { for (const f of oldProgram.getSourceFiles()) { if (this.program.getSourceFileByPath(f.path)) { @@ -232,8 +268,7 @@ namespace ts.server { } } } - - return oldProjectStructureVersion === this.projectStructureVersion; + return hasChanges; } getScriptInfoLSHost(fileName: string) { @@ -392,11 +427,16 @@ namespace ts.server { documentRegistry: ts.DocumentRegistry, hasExplicitListOfFiles: boolean, compilerOptions: CompilerOptions, + private typingOptions: TypingOptions, private wildcardDirectories: Map, languageServiceEnabled: boolean) { super(ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions); } + getTypingOptions() { + return this.typingOptions; + } + getProjectName() { return this.configFileName; } diff --git a/src/server/session.ts b/src/server/session.ts index 5b103b07056..38854f98e90 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -147,6 +147,7 @@ namespace ts.server { } export class Session { + private readonly gcTimer: GcTimer; protected projectService: ProjectService; private errorTimer: any; /*NodeJS.Timer | number*/ private immediateId: any; @@ -165,6 +166,7 @@ namespace ts.server { new ProjectService(host, logger, cancellationToken, useSingleInferredProject, (eventName, project, fileName) => { this.handleEvent(eventName, project, fileName); }); + this.gcTimer = new GcTimer(host, /*delay*/ 15000, logger); } private handleEvent(eventName: string, project: Project, fileName: NormalizedPath) { @@ -1464,6 +1466,7 @@ namespace ts.server { } public onMessage(message: string) { + this.gcTimer.scheduleCollect(); let start: number[]; if (this.logger.hasLevel(LogLevel.requestTime)) { start = this.hrtime(); diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json index e14478c40f4..7b52da7ea39 100644 --- a/src/server/tsconfig.json +++ b/src/server/tsconfig.json @@ -15,6 +15,11 @@ "files": [ "../services/shims.ts", "../services/utilities.ts", + "utilities.ts", + "scriptVersionCache.ts", + "scriptInfo.ts", + "lshost.ts", + "project.ts", "editorServices.ts", "protocol.d.ts", "session.ts", diff --git a/src/server/tsconfig.library.json b/src/server/tsconfig.library.json index 746283720bf..6c0c0a0a484 100644 --- a/src/server/tsconfig.library.json +++ b/src/server/tsconfig.library.json @@ -10,6 +10,13 @@ "types": [] }, "files": [ + "../services/shims.ts", + "../services/utilities.ts", + "utilities.ts", + "scriptVersionCache.ts", + "scriptInfo.ts", + "lshost.ts", + "project.ts", "editorServices.ts", "protocol.d.ts", "session.ts" diff --git a/src/server/typingsCache.ts b/src/server/typingsCache.ts new file mode 100644 index 00000000000..a365aa16f1c --- /dev/null +++ b/src/server/typingsCache.ts @@ -0,0 +1,94 @@ +/// + +namespace ts.server { + export interface ITypingsInstaller { + enqueueInstallTypingsRequest(p: Project): void; + } + + class TypingsCacheEntry { + readonly typingOptions: TypingOptions; + readonly compilerOptions: CompilerOptions; + readonly typings: Path[]; + } + + const emptyArray: any[] = []; + const jsOrDts = [".js", ".d.ts"]; + + function getTypingOptionsForProjects(proj: Project): TypingOptions { + if (proj.projectKind === ProjectKind.Configured) { + return (proj).getTypingOptions(); + } + + const enableAutoDiscovery = + proj.projectKind === ProjectKind.Inferred && + proj.getCompilerOptions().allowJs && + proj.getFileNames().every(f => fileExtensionIsAny(f, jsOrDts)); + + // TODO: add .d.ts files to excludes + return { enableAutoDiscovery, include: emptyArray, exclude: emptyArray }; + } + + function setIsEqualTo(arr1: string[], arr2: string[]): boolean { + if (arr1 === arr2) { + return true; + } + if ((arr1 || emptyArray).length === 0 && (arr2 || emptyArray).length === 0) { + return true; + } + const set: Map = Object.create(null); + let unique = 0; + + for (const v of arr1) { + if (set[v] !== true) { + set[v] = true; + unique++; + } + } + for (const v of arr2) { + if (!hasProperty(set, v)) { + return false; + } + if (set[v] === true) { + set[v] = false; + unique--; + } + } + return unique === 0; + } + + function typingOptionsChanged(opt1: TypingOptions, opt2: TypingOptions): boolean { + return opt1.enableAutoDiscovery !== opt2.enableAutoDiscovery || + !setIsEqualTo(opt1.include, opt2.include) || + !setIsEqualTo(opt1.exclude, opt2.exclude); + } + + function compilerOptionsChanged(opt1: CompilerOptions, opt2: CompilerOptions): boolean { + // TODO: add more relevant properties + return opt1.allowJs != opt2.allowJs; + } + + export class TypingsCache { + private readonly perProjectCache: Map = {}; + + constructor(private readonly installer: ITypingsInstaller) { + } + + getTypingsForProject(project: Project): Path[] { + const typingOptions = getTypingOptionsForProjects(project); + + if(!typingOptions.enableAutoDiscovery) { + return emptyArray; + } + + const entry = this.perProjectCache[project.getProjectName()]; + if (!entry || typingOptionsChanged(typingOptions, entry.typingOptions) || compilerOptionsChanged(project.getCompilerOptions(), entry.compilerOptions)) { + this.installer.enqueueInstallTypingsRequest(project); + } + return entry? entry.typings : emptyArray; + } + + deleteTypingsForProject(project: Project) { + delete this.perProjectCache[project.getProjectName()]; + } + } +} \ No newline at end of file diff --git a/src/server/typingsInstaller/nodeTypingsInstaller.ts b/src/server/typingsInstaller/nodeTypingsInstaller.ts new file mode 100644 index 00000000000..b9b5c7a40a2 --- /dev/null +++ b/src/server/typingsInstaller/nodeTypingsInstaller.ts @@ -0,0 +1,49 @@ +/// + +namespace ts.server.typingsInstaller { + export class NodeTypingsInstaller extends TypingsInstaller { + private execSync: { (command: string, options: { stdio: "ignore" }): any }; + constructor() { + super(); + this.execSync = require("child_process").execSync; + } + + init() { + super.init(); + process.on("install", (req: InstallTypingsRequest) => { + this.install(req); + }) + } + + protected isPackageInstalled(packageName: string) { + try { + this.execSync(`npm list --global --depth=1 ${name}`, { stdio: "ignore" }); + return true; + } + catch (e) { + return false; + } + } + + protected installPackage(packageName: string) { + try { + this.execSync(`npm install --global ${name}`, { stdio: "ignore" }); + return true; + } + catch (e) { + return false; + } + } + + protected getTypingResolutionHost() { + return sys; + } + + protected sendResponse(response: InstallTypingsResponse) { + process.send(response); + } + } + + const installer = new NodeTypingsInstaller(); + installer.init(); +} \ No newline at end of file diff --git a/src/server/typingsInstaller/tsconfig.json b/src/server/typingsInstaller/tsconfig.json new file mode 100644 index 00000000000..57a6066a0d4 --- /dev/null +++ b/src/server/typingsInstaller/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "noImplicitAny": true, + "noImplicitThis": true, + "removeComments": true, + "preserveConstEnums": true, + "pretty": true, + "out": "../../../built/local/typingsInstaller.js", + "sourceMap": true, + "stripInternal": true, + "types": [ + "node" + ] + }, + "files": [ + "../../services/services.ts", + "../utilities.ts", + "typingsInstaller.ts", + "nodeTypingsInstaller.ts" + ] +} diff --git a/src/server/typingsInstaller/typingsInstaller.ts b/src/server/typingsInstaller/typingsInstaller.ts new file mode 100644 index 00000000000..4d1e49d233a --- /dev/null +++ b/src/server/typingsInstaller/typingsInstaller.ts @@ -0,0 +1,75 @@ +/// +/// + + + +namespace ts.server.typingsInstaller { + + const DefaultTsdSettings = JSON.stringify({ + version: "v4", + repo: "DefinitelyTyped/DefinitelyTyped", + ref: "master", + path: "typings" + }, /*replacer*/undefined, /*space*/4); + + export abstract class TypingsInstaller { + + private isTsdInstalled: boolean; + private missingTypings: Map = {}; + + init() { + this.isTsdInstalled = this.isPackageInstalled("tsd"); + if (!this.isTsdInstalled) { + this.isTsdInstalled = this.installPackage("tsd"); + } + } + + install(req: InstallTypingsRequest) { + if (!this.isTsdInstalled) { + return; + } + + const discoverTypingsResult = JsTyping.discoverTypings( + this.getInstallTypingHost(), + req.fileNames, + req.projectRootPath, + req.safeListPath, + req.packageNameToTypingLocation, + req.typingOptions, + req.compilerOptions); + + // respond with whatever cached typings we have now + this.sendResponse(this.createResponse(req, discoverTypingsResult.cachedTypingPaths)); + this.watchFiles(discoverTypingsResult.filesToWatch); + this.installTypings(req, discoverTypingsResult.newTypingNames); + } + + private installTypings(req: InstallTypingsRequest, typingsToInstall: string[]) { + // TODO: install typings and send response when they are ready + const existingTypings = typingsToInstall.fi + const host = this.getInstallTypingHost(); + const tsdPath = combinePaths(req.cachePath, "tsd.json"); + if (!host.fileExists(tsdPath)) { + host.writeFile(tsdPath, DefaultTsdSettings); + } + } + + private watchFiles(files: string[]) { + // TODO: start watching files + } + + private createResponse(request: InstallTypingsRequest, typings: string[]) { + return { + projectName: request.projectName, + typingOptions: request.typingOptions, + compilerOptions: request.compilerOptions, + typings + }; + } + + protected abstract isPackageInstalled(packageName: string): boolean; + protected abstract installPackage(packageName: string): boolean; + protected abstract getInstallTypingHost(): InstallTypingHost; + protected abstract sendResponse(response: InstallTypingsResponse): void; + } +} \ No newline at end of file diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 616286ee8bf..31af02fcceb 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -8,6 +8,28 @@ namespace ts.server { verbose } + export interface InstallTypingsRequest { + 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; + } + + export interface InstallTypingsResponse { + readonly projectName: string; + readonly typingOptions: ts.TypingOptions; + readonly compilerOptions: ts.CompilerOptions; + readonly typings: string[]; + } + + export interface InstallTypingHost extends JsTyping.TypingResolutionHost { + writeFile(path: string, content: string): void; + } + export interface Logger { close(): void; hasLevel(level: LogLevel): boolean; @@ -188,6 +210,7 @@ namespace ts.server { files?: string[]; wildcardDirectories?: Map; compilerOptions?: CompilerOptions; + typingOptions?: TypingOptions; } export function isInferredProjectName(name: string) { @@ -218,4 +241,35 @@ namespace ts.server { cb(); } } + + export class GcTimer { + private timerId: any; + private gc: () => void; + constructor(private readonly host: ServerHost, private readonly delay: number, private readonly logger: Logger) { + if (typeof global !== "undefined" && global.gc) { + this.gc = () => global.gc(); + } + } + + public scheduleCollect() { + if (!this.gc || this.timerId != undefined) { + // no global.gc or collection was already scheduled - skip this request + return; + } + this.timerId = this.host.setTimeout(GcTimer.run, this.delay, this); + } + + private static run(self: GcTimer) { + self.timerId = undefined; + + const log = self.logger.hasLevel(LogLevel.requestTime); + const before = log && self.host.getMemoryUsage(); + + self.gc(); + if (log) { + const after = self.host.getMemoryUsage(); + self.logger.perftrc(`GC::before ${before}, after ${after}`); + } + } + } } \ No newline at end of file