diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index fe7a2095b86..74981bbac35 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -243,9 +243,11 @@ namespace ts.projectSystem { } } - export function createSession(host: server.ServerHost, opts: Partial = {}) { + export function createSession(host: TestServerHost, opts: Partial = {}) { if (opts.typingsInstaller === undefined) { - opts.typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/ 5, host); + const globalTypingsCacheLocation = "/a/data"; + host.ensureFileOrFolder({ path: globalTypingsCacheLocation }); + opts.typingsInstaller = new TestTypingsInstaller(globalTypingsCacheLocation, /*throttleLimit*/ 5, host); } if (opts.eventHandler !== undefined) { diff --git a/src/harness/unittests/typingsInstaller.ts b/src/harness/unittests/typingsInstaller.ts index 5c2f8758916..bdb96c2ec9c 100644 --- a/src/harness/unittests/typingsInstaller.ts +++ b/src/harness/unittests/typingsInstaller.ts @@ -22,7 +22,8 @@ namespace ts.projectSystem { } class Installer extends TestTypingsInstaller { - constructor(host: server.ServerHost, p?: InstallerParams, log?: TI.Log) { + constructor(host: TestServerHost, p?: InstallerParams, log?: TI.Log) { + host.ensureFileOrFolder({ path: (p && p.globalTypingsCacheLocation) || "/a/data" }); super( (p && p.globalTypingsCacheLocation) || "/a/data", (p && p.throttleLimit) || 5, @@ -1053,6 +1054,132 @@ namespace ts.projectSystem { const version2 = proj.getCachedUnresolvedImportsPerFile_TestOnly().getVersion(); assert.notEqual(version1, version2, "set of unresolved imports should change"); }); + + it("expired cache entry (inferred project, should install typings)", () => { + const file1 = { + path: "/a/b/app.js", + content: "" + }; + const packageJson = { + path: "/a/b/package.json", + content: JSON.stringify({ + name: "test", + dependencies: { + jquery: "^3.1.0" + } + }) + }; + const date = new Date(); + date.setFullYear(date.getFullYear() - 1); + const timestamps = { + path: "/a/data/timestamps.json", + content: JSON.stringify({ + entries: { + "@types/jquery": date.getTime() + } + }) + }; + const jquery = { + path: "/a/data/node_modules/@types/jquery/index.d.ts", + content: "declare const $: { x: number }" + }; + const cacheConfig = { + path: "/a/data/package.json", + content: JSON.stringify({ + dependencies: { + "types-registry": "^0.1.317" + }, + devDependencies: { + "@types/jquery": "^3.2.16" + } + }) + }; + const host = createServerHost([file1, packageJson, jquery, timestamps, cacheConfig]); + const installer = new (class extends Installer { + constructor() { + super(host, { typesRegistry: createTypesRegistry("jquery") }); + } + installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction) { + const installedTypings = ["@types/jquery"]; + const typingFiles = [jquery]; + executeCommand(this, host, installedTypings, typingFiles, cb); + } + })(); + + const projectService = createProjectService(host, { useSingleInferredProject: true, typingsInstaller: installer }); + projectService.openClientFile(file1.path); + + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const p = projectService.inferredProjects[0]; + checkProjectActualFiles(p, [file1.path]); + + installer.installAll(/*expectedCount*/ 1); + + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + checkProjectActualFiles(p, [file1.path, jquery.path]); + }); + + it("non-expired cache entry (inferred project, should not install typings)", () => { + const file1 = { + path: "/a/b/app.js", + content: "" + }; + const packageJson = { + path: "/a/b/package.json", + content: JSON.stringify({ + name: "test", + dependencies: { + jquery: "^3.1.0" + } + }) + }; + const timestamps = { + path: "/a/data/timestamps.json", + content: JSON.stringify({ + entries: { + "@types/jquery": Date.now() + } + }) + }; + const cacheConfig = { + path: "/a/data/package.json", + content: JSON.stringify({ + dependencies: { + "types-registry": "^0.1.317" + }, + devDependencies: { + "@types/jquery": "^3.2.16" + } + }) + }; + const jquery = { + path: "/a/data/node_modules/@types/jquery/index.d.ts", + content: "declare const $: { x: number }" + }; + const host = createServerHost([file1, packageJson, timestamps, cacheConfig, jquery]); + const installer = new (class extends Installer { + constructor() { + super(host, { typesRegistry: createTypesRegistry("jquery") }); + } + installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction) { + const installedTypings: string[] = []; + const typingFiles: FileOrFolder[] = []; + executeCommand(this, host, installedTypings, typingFiles, cb); + } + })(); + + const projectService = createProjectService(host, { useSingleInferredProject: true, typingsInstaller: installer }); + projectService.openClientFile(file1.path); + + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const p = projectService.inferredProjects[0]; + checkProjectActualFiles(p, [file1.path]); + + installer.installAll(/*expectedCount*/ 0); + + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + checkProjectActualFiles(p, [file1.path]); + }); }); describe("Validate package name:", () => { @@ -1211,6 +1338,38 @@ namespace ts.projectSystem { filesToWatch: ["/bower_components", "/node_modules"], }); }); + + it("should install expired typings", () => { + const date = new Date(); + date.setFullYear(date.getFullYear() - 1); + + const app = { + path: "/a/app.js", + content: "" + }; + const cachePath = "/a/cache/"; + const commander = { + path: cachePath + "node_modules/@types/commander/index.d.ts", + content: "export let x: number" + }; + const node = { + path: cachePath + "node_modules/@types/node/index.d.ts", + content: "export let y: number" + }; + const host = createServerHost([app]); + const cache = createMapFromTemplate({ + node: { typingLocation: node.path, timestamp: Date.now() }, + commander: { typingLocation: commander.path, timestamp: date.getTime() } + }); + const logger = trackingLogger(); + const result = JsTyping.discoverTypings(host, logger.log, [app.path], getDirectoryPath(app.path), emptySafeList, cache, { enable: true }, ["http", "commander"]); + assert.deepEqual(logger.finish(), [ + 'Inferred typings from unresolved imports: ["node","commander"]', + 'Result: {"cachedTypingPaths":["/a/cache/node_modules/@types/node/index.d.ts"],"newTypingNames":["commander"],"filesToWatch":["/a/bower_components","/a/node_modules"]}', + ]); + assert.deepEqual(result.cachedTypingPaths, [node.path]); + assert.deepEqual(result.newTypingNames, ["commander"]); + }); }); describe("telemetry events", () => { diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 921d4674231..06e1b45a688 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -383,9 +383,11 @@ interface Array {}` ensureFileOrFolder(fileOrDirectory: FileOrFolder, ignoreWatchInvokedWithTriggerAsFileCreate?: boolean) { if (isString(fileOrDirectory.content)) { const file = this.toFile(fileOrDirectory); - Debug.assert(!this.fs.get(file.path)); - const baseFolder = this.ensureFolder(getDirectoryPath(file.fullPath)); - this.addFileOrFolderInFolder(baseFolder, file, ignoreWatchInvokedWithTriggerAsFileCreate); + // file may already exist when updating existing type declaration file + if (!this.fs.get(file.path)) { + const baseFolder = this.ensureFolder(getDirectoryPath(file.fullPath)); + this.addFileOrFolderInFolder(baseFolder, file, ignoreWatchInvokedWithTriggerAsFileCreate); + } } else { const fullPath = getNormalizedAbsolutePath(fileOrDirectory.path, this.currentDirectory); diff --git a/src/services/jsTyping.ts b/src/services/jsTyping.ts index b26b2a00a84..f636de25313 100644 --- a/src/services/jsTyping.ts +++ b/src/services/jsTyping.ts @@ -37,7 +37,8 @@ namespace ts.JsTyping { if (currentMonth) { comparisonDate.setMonth(11); comparisonDate.setFullYear(comparisonDate.getFullYear() - 1); - } else { + } + else { comparisonDate.setMonth(currentMonth - 1); } return !typing || typing.timestamp < comparisonDate.getTime();