Skip to content

Commit f989ef7

Browse files
Merge pull request #21004 from uniqueiniquity/updateATA
Enable typings cache entries to expire and be updated
2 parents 171b68c + 4c89a81 commit f989ef7

File tree

14 files changed

+422
-85
lines changed

14 files changed

+422
-85
lines changed

src/compiler/types.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4118,16 +4118,6 @@ namespace ts {
41184118
[option: string]: string[] | boolean | undefined;
41194119
}
41204120

4121-
export interface DiscoverTypingsInfo {
4122-
fileNames: string[]; // The file names that belong to the same project.
4123-
projectRootPath: string; // The path to the project root directory
4124-
safeListPath: string; // The path used to retrieve the safe list
4125-
packageNameToTypingLocation: Map<string>; // The map of package names to their cached typing locations
4126-
typeAcquisition: TypeAcquisition; // Used to customize the type acquisition process
4127-
compilerOptions: CompilerOptions; // Used as a source for typing inference
4128-
unresolvedImports: ReadonlyArray<string>; // List of unresolved module ids from imports
4129-
}
4130-
41314121
export enum ModuleKind {
41324122
None = 0,
41334123
CommonJS = 1,

src/harness/unittests/tsserverProjectSystem.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ namespace ts.projectSystem {
6363
readonly globalTypingsCacheLocation: string,
6464
throttleLimit: number,
6565
installTypingHost: server.ServerHost,
66-
readonly typesRegistry = createMap<void>(),
66+
readonly typesRegistry = createMap<MapLike<string>>(),
6767
log?: TI.Log) {
6868
super(installTypingHost, globalTypingsCacheLocation, safeList.path, customTypesMap.path, throttleLimit, log);
6969
}
@@ -126,6 +126,25 @@ namespace ts.projectSystem {
126126
return JSON.stringify({ dependencies });
127127
}
128128

129+
export function createTypesRegistry(...list: string[]): Map<MapLike<string>> {
130+
const versionMap = {
131+
"latest": "1.3.0",
132+
"ts2.0": "1.0.0",
133+
"ts2.1": "1.0.0",
134+
"ts2.2": "1.2.0",
135+
"ts2.3": "1.3.0",
136+
"ts2.4": "1.3.0",
137+
"ts2.5": "1.3.0",
138+
"ts2.6": "1.3.0",
139+
"ts2.7": "1.3.0"
140+
};
141+
const map = createMap<MapLike<string>>();
142+
for (const l of list) {
143+
map.set(l, versionMap);
144+
}
145+
return map;
146+
}
147+
129148
export function toExternalFile(fileName: string): protocol.ExternalFile {
130149
return { fileName };
131150
}
@@ -6682,12 +6701,18 @@ namespace ts.projectSystem {
66826701
},
66836702
})
66846703
};
6704+
const typingsCachePackageLockJson: FileOrFolder = {
6705+
path: `${typingsCache}/package-lock.json`,
6706+
content: JSON.stringify({
6707+
dependencies: {
6708+
},
6709+
})
6710+
};
66856711

6686-
const files = [file, packageJsonInCurrentDirectory, packageJsonOfPkgcurrentdirectory, indexOfPkgcurrentdirectory, typingsCachePackageJson];
6712+
const files = [file, packageJsonInCurrentDirectory, packageJsonOfPkgcurrentdirectory, indexOfPkgcurrentdirectory, typingsCachePackageJson, typingsCachePackageLockJson];
66876713
const host = createServerHost(files, { currentDirectory });
66886714

6689-
const typesRegistry = createMap<void>();
6690-
typesRegistry.set("pkgcurrentdirectory", void 0);
6715+
const typesRegistry = createTypesRegistry("pkgcurrentdirectory");
66916716
const typingsInstaller = new TestTypingsInstaller(typingsCache, /*throttleLimit*/ 5, host, typesRegistry);
66926717

66936718
const projectService = createProjectService(host, { typingsInstaller });

src/harness/unittests/typingsInstaller.ts

Lines changed: 246 additions & 18 deletions
Large diffs are not rendered by default.

src/harness/virtualFileSystemWithWatch.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -405,9 +405,11 @@ interface Array<T> {}`
405405
ensureFileOrFolder(fileOrDirectory: FileOrFolder, ignoreWatchInvokedWithTriggerAsFileCreate?: boolean) {
406406
if (isString(fileOrDirectory.content)) {
407407
const file = this.toFile(fileOrDirectory);
408-
Debug.assert(!this.fs.get(file.path));
409-
const baseFolder = this.ensureFolder(getDirectoryPath(file.fullPath));
410-
this.addFileOrFolderInFolder(baseFolder, file, ignoreWatchInvokedWithTriggerAsFileCreate);
408+
// file may already exist when updating existing type declaration file
409+
if (!this.fs.get(file.path)) {
410+
const baseFolder = this.ensureFolder(getDirectoryPath(file.fullPath));
411+
this.addFileOrFolderInFolder(baseFolder, file, ignoreWatchInvokedWithTriggerAsFileCreate);
412+
}
411413
}
412414
else if (isString(fileOrDirectory.symLink)) {
413415
const symLink = this.toSymLink(fileOrDirectory);

src/server/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ namespace ts.server {
253253
private requestMap = createMap<QueuedOperation>(); // Maps operation ID to newest requestQueue entry with that ID
254254
/** We will lazily request the types registry on the first call to `isKnownTypesPackageName` and store it in `typesRegistryCache`. */
255255
private requestedRegistry: boolean;
256-
private typesRegistryCache: Map<void> | undefined;
256+
private typesRegistryCache: Map<MapLike<string>> | undefined;
257257

258258
// This number is essentially arbitrary. Processing more than one typings request
259259
// at a time makes sense, but having too many in the pipe results in a hang

src/server/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ declare namespace ts.server {
7575
/* @internal */
7676
export interface TypesRegistryResponse extends TypingInstallerResponse {
7777
readonly kind: EventTypesRegistry;
78-
readonly typesRegistry: MapLike<void>;
78+
readonly typesRegistry: MapLike<MapLike<string>>;
7979
}
8080

8181
export interface PackageInstalledResponse extends ProjectResponse {

src/server/typingsInstaller/nodeTypingsInstaller.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,15 @@ namespace ts.server.typingsInstaller {
4141
}
4242

4343
interface TypesRegistryFile {
44-
entries: MapLike<void>;
44+
entries: MapLike<MapLike<string>>;
4545
}
4646

47-
function loadTypesRegistryFile(typesRegistryFilePath: string, host: InstallTypingHost, log: Log): Map<void> {
47+
function loadTypesRegistryFile(typesRegistryFilePath: string, host: InstallTypingHost, log: Log): Map<MapLike<string>> {
4848
if (!host.fileExists(typesRegistryFilePath)) {
4949
if (log.isEnabled()) {
5050
log.writeLine(`Types registry file '${typesRegistryFilePath}' does not exist`);
5151
}
52-
return createMap<void>();
52+
return createMap<MapLike<string>>();
5353
}
5454
try {
5555
const content = <TypesRegistryFile>JSON.parse(host.readFile(typesRegistryFilePath));
@@ -59,7 +59,7 @@ namespace ts.server.typingsInstaller {
5959
if (log.isEnabled()) {
6060
log.writeLine(`Error when loading types registry file '${typesRegistryFilePath}': ${(<Error>e).message}, ${(<Error>e).stack}`);
6161
}
62-
return createMap<void>();
62+
return createMap<MapLike<string>>();
6363
}
6464
}
6565

@@ -77,7 +77,7 @@ namespace ts.server.typingsInstaller {
7777
export class NodeTypingsInstaller extends TypingsInstaller {
7878
private readonly nodeExecSync: ExecSync;
7979
private readonly npmPath: string;
80-
readonly typesRegistry: Map<void>;
80+
readonly typesRegistry: Map<MapLike<string>>;
8181

8282
private delayedInitializationError: InitializationFailedResponse | undefined;
8383

@@ -141,7 +141,7 @@ namespace ts.server.typingsInstaller {
141141
this.closeProject(req);
142142
break;
143143
case "typesRegistry": {
144-
const typesRegistry: { [key: string]: void } = {};
144+
const typesRegistry: { [key: string]: MapLike<string> } = {};
145145
this.typesRegistry.forEach((value, key) => {
146146
typesRegistry[key] = value;
147147
});

src/server/typingsInstaller/typingsInstaller.ts

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/// <reference path="../../compiler/core.ts" />
22
/// <reference path="../../compiler/moduleNameResolver.ts" />
33
/// <reference path="../../services/jsTyping.ts"/>
4+
/// <reference path="../../services/semver.ts"/>
45
/// <reference path="../types.ts"/>
56
/// <reference path="../shared.ts"/>
67

@@ -9,6 +10,10 @@ namespace ts.server.typingsInstaller {
910
devDependencies: MapLike<any>;
1011
}
1112

13+
interface NpmLock {
14+
dependencies: { [packageName: string]: { version: string } };
15+
}
16+
1217
export interface Log {
1318
isEnabled(): boolean;
1419
writeLine(text: string): void;
@@ -42,7 +47,7 @@ namespace ts.server.typingsInstaller {
4247
}
4348

4449
export abstract class TypingsInstaller {
45-
private readonly packageNameToTypingLocation: Map<string> = createMap<string>();
50+
private readonly packageNameToTypingLocation: Map<JsTyping.CachedTyping> = createMap<JsTyping.CachedTyping>();
4651
private readonly missingTypingsSet: Map<true> = createMap<true>();
4752
private readonly knownCachesSet: Map<true> = createMap<true>();
4853
private readonly projectWatchers: Map<FileWatcher[]> = createMap<FileWatcher[]>();
@@ -52,7 +57,7 @@ namespace ts.server.typingsInstaller {
5257
private installRunCount = 1;
5358
private inFlightRequestCount = 0;
5459

55-
abstract readonly typesRegistry: Map<void>;
60+
abstract readonly typesRegistry: Map<MapLike<string>>;
5661

5762
constructor(
5863
protected readonly installTypingHost: InstallTypingHost,
@@ -117,7 +122,8 @@ namespace ts.server.typingsInstaller {
117122
this.safeList,
118123
this.packageNameToTypingLocation,
119124
req.typeAcquisition,
120-
req.unresolvedImports);
125+
req.unresolvedImports,
126+
this.typesRegistry);
121127

122128
if (this.log.isEnabled()) {
123129
this.log.writeLine(`Finished typings discovery: ${JSON.stringify(discoverTypingsResult)}`);
@@ -156,23 +162,30 @@ namespace ts.server.typingsInstaller {
156162
if (this.log.isEnabled()) {
157163
this.log.writeLine(`Processing cache location '${cacheLocation}'`);
158164
}
159-
if (this.knownCachesSet.get(cacheLocation)) {
165+
if (this.knownCachesSet.has(cacheLocation)) {
160166
if (this.log.isEnabled()) {
161167
this.log.writeLine(`Cache location was already processed...`);
162168
}
163169
return;
164170
}
165171
const packageJson = combinePaths(cacheLocation, "package.json");
172+
const packageLockJson = combinePaths(cacheLocation, "package-lock.json");
166173
if (this.log.isEnabled()) {
167174
this.log.writeLine(`Trying to find '${packageJson}'...`);
168175
}
169-
if (this.installTypingHost.fileExists(packageJson)) {
176+
if (this.installTypingHost.fileExists(packageJson) && this.installTypingHost.fileExists(packageLockJson)) {
170177
const npmConfig = <NpmConfig>JSON.parse(this.installTypingHost.readFile(packageJson));
178+
const npmLock = <NpmLock>JSON.parse(this.installTypingHost.readFile(packageLockJson));
171179
if (this.log.isEnabled()) {
172180
this.log.writeLine(`Loaded content of '${packageJson}': ${JSON.stringify(npmConfig)}`);
181+
this.log.writeLine(`Loaded content of '${packageLockJson}'`);
173182
}
174-
if (npmConfig.devDependencies) {
183+
if (npmConfig.devDependencies && npmLock.dependencies) {
175184
for (const key in npmConfig.devDependencies) {
185+
if (!hasProperty(npmLock.dependencies, key)) {
186+
// if package in package.json but not package-lock.json, skip adding to cache so it is reinstalled on next use
187+
continue;
188+
}
176189
// key is @types/<package name>
177190
const packageName = getBaseFileName(key);
178191
if (!packageName) {
@@ -184,18 +197,23 @@ namespace ts.server.typingsInstaller {
184197
continue;
185198
}
186199
const existingTypingFile = this.packageNameToTypingLocation.get(packageName);
187-
if (existingTypingFile === typingFile) {
188-
continue;
189-
}
190200
if (existingTypingFile) {
201+
if (existingTypingFile.typingLocation === typingFile) {
202+
continue;
203+
}
204+
191205
if (this.log.isEnabled()) {
192206
this.log.writeLine(`New typing for package ${packageName} from '${typingFile}' conflicts with existing typing file '${existingTypingFile}'`);
193207
}
194208
}
195209
if (this.log.isEnabled()) {
196210
this.log.writeLine(`Adding entry into typings cache: '${packageName}' => '${typingFile}'`);
197211
}
198-
this.packageNameToTypingLocation.set(packageName, typingFile);
212+
const info = getProperty(npmLock.dependencies, key);
213+
const version = info && info.version;
214+
const semver = Semver.parse(version);
215+
const newTyping: JsTyping.CachedTyping = { typingLocation: typingFile, version: semver };
216+
this.packageNameToTypingLocation.set(packageName, newTyping);
199217
}
200218
}
201219
}
@@ -211,10 +229,6 @@ namespace ts.server.typingsInstaller {
211229
if (this.log.isEnabled()) this.log.writeLine(`'${typing}' is in missingTypingsSet - skipping...`);
212230
return false;
213231
}
214-
if (this.packageNameToTypingLocation.get(typing)) {
215-
if (this.log.isEnabled()) this.log.writeLine(`'${typing}' already has a typing - skipping...`);
216-
return false;
217-
}
218232
const validationResult = JsTyping.validatePackageName(typing);
219233
if (validationResult !== JsTyping.PackageNameValidationResult.Ok) {
220234
// add typing name to missing set so we won't process it again
@@ -226,6 +240,10 @@ namespace ts.server.typingsInstaller {
226240
if (this.log.isEnabled()) this.log.writeLine(`Entry for package '${typing}' does not exist in local types registry - skipping...`);
227241
return false;
228242
}
243+
if (this.packageNameToTypingLocation.get(typing) && JsTyping.isTypingUpToDate(this.packageNameToTypingLocation.get(typing), this.typesRegistry.get(typing))) {
244+
if (this.log.isEnabled()) this.log.writeLine(`'${typing}' already has an up-to-date typing - skipping...`);
245+
return false;
246+
}
229247
return true;
230248
});
231249
}
@@ -294,9 +312,12 @@ namespace ts.server.typingsInstaller {
294312
this.missingTypingsSet.set(packageName, true);
295313
continue;
296314
}
297-
if (!this.packageNameToTypingLocation.has(packageName)) {
298-
this.packageNameToTypingLocation.set(packageName, typingFile);
299-
}
315+
316+
// packageName is guaranteed to exist in typesRegistry by filterTypings
317+
const distTags = this.typesRegistry.get(packageName);
318+
const newVersion = Semver.parse(distTags[`ts${ts.versionMajorMinor}`] || distTags[latestDistTag]);
319+
const newTyping: JsTyping.CachedTyping = { typingLocation: typingFile, version: newVersion };
320+
this.packageNameToTypingLocation.set(packageName, newTyping);
300321
installedTypingFiles.push(typingFile);
301322
}
302323
if (this.log.isEnabled()) {
@@ -390,4 +411,6 @@ namespace ts.server.typingsInstaller {
390411
export function typingsName(packageName: string): string {
391412
return `@types/${packageName}@ts${versionMajorMinor}`;
392413
}
414+
415+
const latestDistTag = "latest";
393416
}

src/services/jsTyping.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
/// <reference path='../compiler/types.ts' />
55
/// <reference path='../compiler/core.ts' />
66
/// <reference path='../compiler/commandLineParser.ts' />
7+
/// <reference path='../services/semver.ts' />
78

89
/* @internal */
910
namespace ts.JsTyping {
@@ -26,6 +27,17 @@ namespace ts.JsTyping {
2627
typings?: string;
2728
}
2829

30+
export interface CachedTyping {
31+
typingLocation: string;
32+
version: Semver;
33+
}
34+
35+
/* @internal */
36+
export function isTypingUpToDate(cachedTyping: JsTyping.CachedTyping, availableTypingVersions: MapLike<string>) {
37+
const availableVersion = Semver.parse(getProperty(availableTypingVersions, `ts${ts.versionMajorMinor}`) || getProperty(availableTypingVersions, "latest"));
38+
return !availableVersion.greaterThan(cachedTyping.version);
39+
}
40+
2941
/* @internal */
3042
export const nodeCoreModuleList: ReadonlyArray<string> = [
3143
"buffer", "querystring", "events", "http", "cluster",
@@ -60,7 +72,7 @@ namespace ts.JsTyping {
6072
* @param fileNames are the file names that belong to the same project
6173
* @param projectRootPath is the path to the project root directory
6274
* @param safeListPath is the path used to retrieve the safe list
63-
* @param packageNameToTypingLocation is the map of package names to their cached typing locations
75+
* @param packageNameToTypingLocation is the map of package names to their cached typing locations and installed versions
6476
* @param typeAcquisition is used to customize the typing acquisition process
6577
* @param compilerOptions are used as a source for typing inference
6678
*/
@@ -70,9 +82,10 @@ namespace ts.JsTyping {
7082
fileNames: string[],
7183
projectRootPath: Path,
7284
safeList: SafeList,
73-
packageNameToTypingLocation: ReadonlyMap<string>,
85+
packageNameToTypingLocation: ReadonlyMap<CachedTyping>,
7486
typeAcquisition: TypeAcquisition,
75-
unresolvedImports: ReadonlyArray<string>):
87+
unresolvedImports: ReadonlyArray<string>,
88+
typesRegistry: ReadonlyMap<MapLike<string>>):
7689
{ cachedTypingPaths: string[], newTypingNames: string[], filesToWatch: string[] } {
7790

7891
if (!typeAcquisition || !typeAcquisition.enable) {
@@ -122,9 +135,9 @@ namespace ts.JsTyping {
122135
addInferredTypings(module, "Inferred typings from unresolved imports");
123136
}
124137
// Add the cached typing locations for inferred typings that are already installed
125-
packageNameToTypingLocation.forEach((typingLocation, name) => {
126-
if (inferredTypings.has(name) && inferredTypings.get(name) === undefined) {
127-
inferredTypings.set(name, typingLocation);
138+
packageNameToTypingLocation.forEach((typing, name) => {
139+
if (inferredTypings.has(name) && inferredTypings.get(name) === undefined && isTypingUpToDate(typing, typesRegistry.get(name))) {
140+
inferredTypings.set(name, typing.typingLocation);
128141
}
129142
});
130143

0 commit comments

Comments
 (0)