diff --git a/src/harness/unittests/typingsInstaller.ts b/src/harness/unittests/typingsInstaller.ts index cbeab8f7a97..cf3d82ca3d7 100644 --- a/src/harness/unittests/typingsInstaller.ts +++ b/src/harness/unittests/typingsInstaller.ts @@ -1081,16 +1081,6 @@ namespace ts.projectSystem { } }) }; - 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 }" @@ -1102,11 +1092,21 @@ namespace ts.projectSystem { "types-registry": "^0.1.317" }, devDependencies: { - "@types/jquery": "^3.2.16" + "@types/jquery": "^1.0.0" } }) }; - const host = createServerHost([file1, packageJson, jquery, timestamps, cacheConfig]); + const cacheLockConfig = { + path: "/a/data/package-lock.json", + content: JSON.stringify({ + dependencies: { + "@types/jquery": { + version: "1.0.0" + } + } + }) + }; + const host = createServerHost([file1, packageJson, jquery, cacheConfig, cacheLockConfig]); const installer = new (class extends Installer { constructor() { super(host, { typesRegistry: createTypesRegistry("jquery") }); @@ -1129,7 +1129,6 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { inferredProjects: 1 }); checkProjectActualFiles(p, [file1.path, jquery.path]); - assert(host.readFile(timestamps.path) !== JSON.stringify({ entries: { "@types/jquery": date.getTime() } }), "timestamps content should be updated"); }); it("non-expired cache entry (inferred project, should not install typings)", () => { @@ -1282,7 +1281,7 @@ namespace ts.projectSystem { const host = createServerHost([app, jquery, chroma]); const logger = trackingLogger(); - const result = JsTyping.discoverTypings(host, logger.log, [app.path, jquery.path, chroma.path], getDirectoryPath(app.path), safeList, emptyMap, { enable: true }, emptyArray); + const result = JsTyping.discoverTypings(host, logger.log, [app.path, jquery.path, chroma.path], getDirectoryPath(app.path), safeList, emptyMap, { enable: true }, emptyArray, emptyMap); const finish = logger.finish(); assert.deepEqual(finish, [ 'Inferred typings from file names: ["jquery","chroma-js"]', @@ -1302,7 +1301,7 @@ namespace ts.projectSystem { for (const name of JsTyping.nodeCoreModuleList) { const logger = trackingLogger(); - const result = JsTyping.discoverTypings(host, logger.log, [f.path], getDirectoryPath(f.path), emptySafeList, cache, { enable: true }, [name, "somename"]); + const result = JsTyping.discoverTypings(host, logger.log, [f.path], getDirectoryPath(f.path), emptySafeList, cache, { enable: true }, [name, "somename"], emptyMap); assert.deepEqual(logger.finish(), [ 'Inferred typings from unresolved imports: ["node","somename"]', 'Result: {"cachedTypingPaths":[],"newTypingNames":["node","somename"],"filesToWatch":["/a/b/bower_components","/a/b/node_modules"]}', @@ -1321,9 +1320,10 @@ namespace ts.projectSystem { content: "" }; const host = createServerHost([f, node]); - const cache = createMapFromTemplate({ node: { typingLocation: node.path, timestamp: Date.now(), version: new Semver(1, 0, 0, /*isPrerelease*/ false) } }); + const cache = createMapFromTemplate({ node: { typingLocation: node.path, version: new Semver(1, 3, 0, /*isPrerelease*/ false) } }); + const registry = createTypesRegistry("node"); const logger = trackingLogger(); - const result = JsTyping.discoverTypings(host, logger.log, [f.path], getDirectoryPath(f.path), emptySafeList, cache, { enable: true }, ["fs", "bar"]); + const result = JsTyping.discoverTypings(host, logger.log, [f.path], getDirectoryPath(f.path), emptySafeList, cache, { enable: true }, ["fs", "bar"], registry); assert.deepEqual(logger.finish(), [ 'Inferred typings from unresolved imports: ["node","bar"]', 'Result: {"cachedTypingPaths":["/a/b/node.d.ts"],"newTypingNames":["bar"],"filesToWatch":["/a/b/bower_components","/a/b/node_modules"]}', @@ -1348,7 +1348,7 @@ namespace ts.projectSystem { const host = createServerHost([app, a, b]); const cache = createMap(); const logger = trackingLogger(); - const result = JsTyping.discoverTypings(host, logger.log, [app.path], getDirectoryPath(app.path), emptySafeList, cache, { enable: true }, /*unresolvedImports*/ []); + const result = JsTyping.discoverTypings(host, logger.log, [app.path], getDirectoryPath(app.path), emptySafeList, cache, { enable: true }, /*unresolvedImports*/ [], emptyMap); assert.deepEqual(logger.finish(), [ 'Searching for typing names in /node_modules; all files: ["/node_modules/a/package.json"]', ' Found package names: ["a"]', @@ -1363,9 +1363,6 @@ namespace ts.projectSystem { }); it("should install expired typings", () => { - const date = new Date(); - date.setFullYear(date.getFullYear() - 1); - const app = { path: "/a/app.js", content: "" @@ -1381,11 +1378,12 @@ namespace ts.projectSystem { }; const host = createServerHost([app]); const cache = createMapFromTemplate({ - node: { typingLocation: node.path, timestamp: Date.now(), version: new Semver(1, 0, 0, /*isPrerelease*/ false) }, - commander: { typingLocation: commander.path, timestamp: date.getTime(), version: new Semver(1, 0, 0, /*isPrerelease*/ false) } + node: { typingLocation: node.path, version: new Semver(1, 3, 0, /*isPrerelease*/ false) }, + commander: { typingLocation: commander.path, version: new Semver(1, 0, 0, /*isPrerelease*/ false) } }); + const registry = createTypesRegistry("node", "commander"); const logger = trackingLogger(); - const result = JsTyping.discoverTypings(host, logger.log, [app.path], getDirectoryPath(app.path), emptySafeList, cache, { enable: true }, ["http", "commander"]); + const result = JsTyping.discoverTypings(host, logger.log, [app.path], getDirectoryPath(app.path), emptySafeList, cache, { enable: true }, ["http", "commander"], registry); 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"]}', diff --git a/src/server/types.ts b/src/server/types.ts index 161fb00a6ae..6f773955d45 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -128,6 +128,5 @@ declare namespace ts.server { writeFile(path: string, content: string): void; createDirectory(path: string): void; watchFile?(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher; - getModifiedTime?(path: string): Date; } } \ No newline at end of file diff --git a/src/server/typingsInstaller/typingsInstaller.ts b/src/server/typingsInstaller/typingsInstaller.ts index f3048795229..18890d8af05 100644 --- a/src/server/typingsInstaller/typingsInstaller.ts +++ b/src/server/typingsInstaller/typingsInstaller.ts @@ -46,62 +46,10 @@ namespace ts.server.typingsInstaller { onRequestCompleted: RequestCompletedAction; } - const timestampsFileName = "timestamps.json"; - type TypingsTimestamps = MapLike; - interface TypeDeclarationTimestampFile { - // entries maps from package names (e.g. "@types/node") to timestamp values (as produced by Date#getTime) - entries: TypingsTimestamps; - } - - function loadTypeDeclarationTimestampFile(typeDeclarationTimestampFilePath: string, host: InstallTypingHost, log: Log): TypingsTimestamps { - try { - if (log.isEnabled()) { - log.writeLine("Loading type declaration timestamp file."); - } - const content = JSON.parse(host.readFile(typeDeclarationTimestampFilePath)); - return content.entries || {}; - } - catch (e) { - if (log.isEnabled()) { - log.writeLine(`Error when loading type declaration timestamp file '${typeDeclarationTimestampFilePath}': ${(e).message}, ${(e).stack}`); - } - // If file cannot be read, we update all requested type declarations. - return {}; - } - } - - function updateTypeDeclarationTimestampFile(typeDeclarationTimestampFilePath: string, timestampsInProcess: TypingsTimestamps, host: InstallTypingHost, log: Log): TypingsTimestamps { - const timestampsOnDisk = loadTypeDeclarationTimestampFile(typeDeclarationTimestampFilePath, host, log); - for (const packageName in timestampsOnDisk) { - const timestampForPackageInProcess = getProperty(timestampsInProcess, packageName); - if (timestampForPackageInProcess) { - timestampsInProcess[packageName] = Math.max(timestampForPackageInProcess, timestampsOnDisk[packageName]); - } - else { - timestampsInProcess[packageName] = timestampsOnDisk[packageName]; - } - } - return timestampsInProcess; - } - - function writeTypeDeclarationTimestampFile(typeDeclarationTimestampFilePath: string, newContents: TypeDeclarationTimestampFile, host: InstallTypingHost, log: Log): void { - try { - if (log.isEnabled()) { - log.writeLine("Writing type declaration timestamp file."); - } - host.writeFile(typeDeclarationTimestampFilePath, JSON.stringify(newContents)); - } - catch (e) { - if (log.isEnabled()) { - log.writeLine(`Error when writing type declaration timestamp file '${typeDeclarationTimestampFilePath}': ${(e).message}, ${(e).stack}`); - } - } - } - export abstract class TypingsInstaller { private readonly packageNameToTypingLocation: Map = createMap(); private readonly missingTypingsSet: Map = createMap(); - private readonly knownCacheToTimestamps: Map = createMap(); + private readonly knownCachesSet: Map = createMap(); private readonly projectWatchers: Map = createMap(); private safeList: JsTyping.SafeList | undefined; readonly pendingRunRequests: PendingRequest[] = []; @@ -156,12 +104,11 @@ namespace ts.server.typingsInstaller { } // load existing typing information from the cache - const timestampsFilePath = combinePaths(req.cachePath || this.globalCachePath, timestampsFileName); if (req.cachePath) { if (this.log.isEnabled()) { this.log.writeLine(`Request specifies cache path '${req.cachePath}', loading cached information...`); } - this.processCacheLocation(req.cachePath, timestampsFilePath); + this.processCacheLocation(req.cachePath); } if (this.safeList === undefined) { @@ -175,7 +122,8 @@ namespace ts.server.typingsInstaller { this.safeList, this.packageNameToTypingLocation, req.typeAcquisition, - req.unresolvedImports); + req.unresolvedImports, + this.typesRegistry); if (this.log.isEnabled()) { this.log.writeLine(`Finished typings discovery: ${JSON.stringify(discoverTypingsResult)}`); @@ -186,7 +134,7 @@ namespace ts.server.typingsInstaller { // install typings if (discoverTypingsResult.newTypingNames.length) { - this.installTypings(req, req.cachePath || this.globalCachePath, discoverTypingsResult.cachedTypingPaths, discoverTypingsResult.newTypingNames, timestampsFilePath); + this.installTypings(req, req.cachePath || this.globalCachePath, discoverTypingsResult.cachedTypingPaths, discoverTypingsResult.newTypingNames); } else { this.sendResponse(this.createSetTypings(req, discoverTypingsResult.cachedTypingPaths)); @@ -210,17 +158,16 @@ namespace ts.server.typingsInstaller { this.safeList = JsTyping.loadSafeList(this.installTypingHost, this.safeListPath); } - private processCacheLocation(cacheLocation: string, timestampsFilePath?: string) { + private processCacheLocation(cacheLocation: string) { if (this.log.isEnabled()) { this.log.writeLine(`Processing cache location '${cacheLocation}'`); } - if (this.knownCacheToTimestamps.has(cacheLocation)) { + if (this.knownCachesSet.has(cacheLocation)) { if (this.log.isEnabled()) { this.log.writeLine(`Cache location was already processed...`); } return; } - const typeDeclarationTimestamps = loadTypeDeclarationTimestampFile(timestampsFilePath || combinePaths(cacheLocation, timestampsFileName), this.installTypingHost, this.log); const packageJson = combinePaths(cacheLocation, "package.json"); const packageLockJson = combinePaths(cacheLocation, "package-lock.json"); if (this.log.isEnabled()) { @@ -258,20 +205,10 @@ namespace ts.server.typingsInstaller { if (this.log.isEnabled()) { this.log.writeLine(`Adding entry into typings cache: '${packageName}' => '${typingFile}'`); } - if (getProperty(typeDeclarationTimestamps, key) === undefined) { - // getModifiedTime is only undefined if we were to use the ChakraHost, but we never do in this scenario - // defaults to old behavior of never updating if we ever use a host without getModifiedTime in the future - const timestamp = this.installTypingHost.getModifiedTime === undefined ? Date.now() : this.installTypingHost.getModifiedTime(typingFile).getTime(); - typeDeclarationTimestamps[key] = timestamp; - if (this.log.isEnabled()) { - this.log.writeLine(`Adding entry into timestamp cache: '${key}' => '${timestamp}'`); - } - } const info = getProperty(npmLock.dependencies, key); const version = info && info.version; const semver = Semver.parse(version); - // timestamp guaranteed to not be undefined by above check - const newTyping: JsTyping.CachedTyping = { typingLocation: typingFile, timestamp: getProperty(typeDeclarationTimestamps, key), version: semver }; + const newTyping: JsTyping.CachedTyping = { typingLocation: typingFile, version: semver }; this.packageNameToTypingLocation.set(packageName, newTyping); } } @@ -279,7 +216,7 @@ namespace ts.server.typingsInstaller { if (this.log.isEnabled()) { this.log.writeLine(`Finished processing cache location '${cacheLocation}'`); } - this.knownCacheToTimestamps.set(cacheLocation, typeDeclarationTimestamps); + this.knownCachesSet.set(cacheLocation, true); } private filterTypings(typingsToInstall: ReadonlyArray): ReadonlyArray { @@ -299,17 +236,12 @@ namespace ts.server.typingsInstaller { if (this.log.isEnabled()) this.log.writeLine(`Entry for package '${typing}' does not exist in local types registry - skipping...`); return false; } - if (this.packageNameToTypingLocation.get(typing) && isTypingUpToDate(this.packageNameToTypingLocation.get(typing), this.typesRegistry.get(typing))) { + if (this.packageNameToTypingLocation.get(typing) && JsTyping.isTypingUpToDate(this.packageNameToTypingLocation.get(typing), this.typesRegistry.get(typing))) { if (this.log.isEnabled()) this.log.writeLine(`'${typing}' already has an up-to-date typing - skipping...`); return false; } return true; }); - - function isTypingUpToDate(cachedTyping: JsTyping.CachedTyping, availableTypingVersions: MapLike) { - const availableVersion = Semver.parse(getProperty(availableTypingVersions, `ts${ts.version}`)); - return !availableVersion.greaterThan(cachedTyping.version); - } } protected ensurePackageDirectoryExists(directory: string) { @@ -326,7 +258,7 @@ namespace ts.server.typingsInstaller { } } - private installTypings(req: DiscoverTypings, cachePath: string, currentlyCachedTypings: string[], typingsToInstall: string[], timestampsFilePath: string) { + private installTypings(req: DiscoverTypings, cachePath: string, currentlyCachedTypings: string[], typingsToInstall: string[]) { if (this.log.isEnabled()) { this.log.writeLine(`Installing typings ${JSON.stringify(typingsToInstall)}`); } @@ -369,9 +301,7 @@ namespace ts.server.typingsInstaller { if (this.log.isEnabled()) { this.log.writeLine(`Installed typings ${JSON.stringify(scopedTypings)}`); } - const typeDeclarationTimestamps = this.knownCacheToTimestamps.get(cachePath); const installedTypingFiles: string[] = []; - const typesPackageName = (packageName: string) => `@types/${packageName}`; for (const packageName of filteredTypings) { const typingFile = typingToFileName(cachePath, packageName, this.installTypingHost, this.log); if (!typingFile) { @@ -379,22 +309,15 @@ namespace ts.server.typingsInstaller { continue; } - const newTimestamp = Date.now(); const newVersion = Semver.parse(this.typesRegistry.get(packageName)[`ts${ts.versionMajorMinor}`]); - const newTyping: JsTyping.CachedTyping = { typingLocation: typingFile, timestamp: newTimestamp, version: newVersion }; + const newTyping: JsTyping.CachedTyping = { typingLocation: typingFile, version: newVersion }; this.packageNameToTypingLocation.set(packageName, newTyping); - typeDeclarationTimestamps[typesPackageName(packageName)] = newTimestamp; installedTypingFiles.push(typingFile); } if (this.log.isEnabled()) { this.log.writeLine(`Installed typing files ${JSON.stringify(installedTypingFiles)}`); } - const updatedTypeDeclarationTimestamps = updateTypeDeclarationTimestampFile(timestampsFilePath, typeDeclarationTimestamps, this.installTypingHost, this.log); - const newFileContents: TypeDeclarationTimestampFile = { entries: updatedTypeDeclarationTimestamps }; - writeTypeDeclarationTimestampFile(timestampsFilePath, newFileContents, this.installTypingHost, this.log); - this.knownCacheToTimestamps.set(cachePath, updatedTypeDeclarationTimestamps); - this.sendResponse(this.createSetTypings(req, currentlyCachedTypings.concat(installedTypingFiles))); } finally { diff --git a/src/services/jsTyping.ts b/src/services/jsTyping.ts index 75df1a7ea1b..3900a290bce 100644 --- a/src/services/jsTyping.ts +++ b/src/services/jsTyping.ts @@ -29,13 +29,13 @@ namespace ts.JsTyping { export interface CachedTyping { typingLocation: string; - timestamp: number; version: Semver; } - export function isTypingExpired(typing: JsTyping.CachedTyping | undefined) { - const msPerMonth = 1000 * 60 * 60 * 24 * 30; // ms/second * second/minute * minutes/hour * hours/day * days/month - return !typing || typing.timestamp < Date.now() - msPerMonth; + /* @internal */ + export function isTypingUpToDate(cachedTyping: JsTyping.CachedTyping, availableTypingVersions: MapLike) { + const availableVersion = Semver.parse(getProperty(availableTypingVersions, `ts${ts.versionMajorMinor}`)); + return !availableVersion.greaterThan(cachedTyping.version); } /* @internal */ @@ -72,7 +72,7 @@ namespace ts.JsTyping { * @param fileNames are the file names that belong to the same project * @param projectRootPath is the path to the project root directory * @param safeListPath is the path used to retrieve the safe list - * @param packageNameToTypingLocation is the map of package names to their cached typing locations and time of caching and versions + * @param packageNameToTypingLocation is the map of package names to their cached typing locations and installed versions * @param typeAcquisition is used to customize the typing acquisition process * @param compilerOptions are used as a source for typing inference */ @@ -84,7 +84,8 @@ namespace ts.JsTyping { safeList: SafeList, packageNameToTypingLocation: ReadonlyMap, typeAcquisition: TypeAcquisition, - unresolvedImports: ReadonlyArray): + unresolvedImports: ReadonlyArray, + typesRegistry: ReadonlyMap>): { cachedTypingPaths: string[], newTypingNames: string[], filesToWatch: string[] } { if (!typeAcquisition || !typeAcquisition.enable) { @@ -135,7 +136,7 @@ namespace ts.JsTyping { } // Add the cached typing locations for inferred typings that are already installed packageNameToTypingLocation.forEach((typing, name) => { - if (inferredTypings.has(name) && inferredTypings.get(name) === undefined && !isTypingExpired(typing)) { + if (inferredTypings.has(name) && inferredTypings.get(name) === undefined && isTypingUpToDate(typing, typesRegistry.get(name))) { inferredTypings.set(name, typing.typingLocation); } }); diff --git a/src/services/shims.ts b/src/services/shims.ts index d4bad1908e9..1b2f84035c0 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -28,10 +28,11 @@ namespace ts { fileNames: string[]; // The file names that belong to the same project. projectRootPath: string; // The path to the project root directory safeListPath: string; // The path used to retrieve the safe list - packageNameToTypingLocation: Map; // The map of package names to their cached typing locations + packageNameToTypingLocation: Map; // The map of package names to their cached typing locations and installed versions typeAcquisition: TypeAcquisition; // Used to customize the type acquisition process compilerOptions: CompilerOptions; // Used as a source for typing inference unresolvedImports: ReadonlyArray; // List of unresolved module ids from imports + typesRegistry: ReadonlyMap>; // The map of available typings in npm to maps of TS versions to their latest supported versions } export interface ScriptSnapshotShim { @@ -1171,7 +1172,8 @@ namespace ts { this.safeList, info.packageNameToTypingLocation, info.typeAcquisition, - info.unresolvedImports); + info.unresolvedImports, + info.typesRegistry); }); } }