Skip to content

Commit e72ea6f

Browse files
Update installed types if older than those listed in the registry
1 parent 2332434 commit e72ea6f

File tree

9 files changed

+137
-30
lines changed

9 files changed

+137
-30
lines changed

src/harness/unittests/tsserverProjectSystem.ts

Lines changed: 1 addition & 1 deletion
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
}

src/harness/unittests/typingsInstaller.ts

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/// <reference path="../harness.ts" />
22
/// <reference path="./tsserverProjectSystem.ts" />
33
/// <reference path="../../server/typingsInstaller/typingsInstaller.ts" />
4+
/// <reference path="../../services/semver.ts" />
45

56
namespace ts.projectSystem {
67
import TI = server.typingsInstaller;
@@ -10,13 +11,24 @@ namespace ts.projectSystem {
1011
interface InstallerParams {
1112
globalTypingsCacheLocation?: string;
1213
throttleLimit?: number;
13-
typesRegistry?: Map<void>;
14+
typesRegistry?: Map<MapLike<string>>;
1415
}
1516

16-
function createTypesRegistry(...list: string[]): Map<void> {
17-
const map = createMap<void>();
17+
function createTypesRegistry(...list: string[]): Map<MapLike<string>> {
18+
const versionMap = {
19+
"latest": "1.3.0",
20+
"ts2.0": "1.0.0",
21+
"ts2.1": "1.0.0",
22+
"ts2.2": "1.2.0",
23+
"ts2.3": "1.3.0",
24+
"ts2.4": "1.3.0",
25+
"ts2.5": "1.3.0",
26+
"ts2.6": "1.3.0",
27+
"ts2.7": "1.3.0"
28+
};
29+
const map = createMap<MapLike<string>>();
1830
for (const l of list) {
19-
map.set(l, undefined);
31+
map.set(l, versionMap);
2032
}
2133
return map;
2234
}
@@ -51,7 +63,7 @@ namespace ts.projectSystem {
5163
const logs: string[] = [];
5264
return {
5365
log(message) {
54-
logs.push(message);
66+
logs.push(message);
5567
},
5668
finish() {
5769
return logs;
@@ -1149,15 +1161,25 @@ namespace ts.projectSystem {
11491161
"types-registry": "^0.1.317"
11501162
},
11511163
devDependencies: {
1152-
"@types/jquery": "^3.2.16"
1164+
"@types/jquery": "^1.3.0"
1165+
}
1166+
})
1167+
};
1168+
const cacheLockConfig = {
1169+
path: "/a/data/package-lock.json",
1170+
content: JSON.stringify({
1171+
dependencies: {
1172+
"@types/jquery": {
1173+
version: "1.3.0"
1174+
}
11531175
}
11541176
})
11551177
};
11561178
const jquery = {
11571179
path: "/a/data/node_modules/@types/jquery/index.d.ts",
11581180
content: "declare const $: { x: number }"
11591181
};
1160-
const host = createServerHost([file1, packageJson, timestamps, cacheConfig, jquery]);
1182+
const host = createServerHost([file1, packageJson, timestamps, cacheConfig, cacheLockConfig, jquery]);
11611183
const installer = new (class extends Installer {
11621184
constructor() {
11631185
super(host, { typesRegistry: createTypesRegistry("jquery") });
@@ -1299,7 +1321,7 @@ namespace ts.projectSystem {
12991321
content: ""
13001322
};
13011323
const host = createServerHost([f, node]);
1302-
const cache = createMapFromTemplate<JsTyping.CachedTyping>({ node: { typingLocation: node.path, timestamp: Date.now() } });
1324+
const cache = createMapFromTemplate<JsTyping.CachedTyping>({ node: { typingLocation: node.path, timestamp: Date.now(), version: new Semver(1, 0, 0, /*isPrerelease*/ false) } });
13031325
const logger = trackingLogger();
13041326
const result = JsTyping.discoverTypings(host, logger.log, [f.path], getDirectoryPath(<Path>f.path), emptySafeList, cache, { enable: true }, ["fs", "bar"]);
13051327
assert.deepEqual(logger.finish(), [
@@ -1359,8 +1381,8 @@ namespace ts.projectSystem {
13591381
};
13601382
const host = createServerHost([app]);
13611383
const cache = createMapFromTemplate<JsTyping.CachedTyping>({
1362-
node: { typingLocation: node.path, timestamp: Date.now() },
1363-
commander: { typingLocation: commander.path, timestamp: date.getTime() }
1384+
node: { typingLocation: node.path, timestamp: Date.now(), version: new Semver(1, 0, 0, /*isPrerelease*/ false) },
1385+
commander: { typingLocation: commander.path, timestamp: date.getTime(), version: new Semver(1, 0, 0, /*isPrerelease*/ false) }
13641386
});
13651387
const logger = trackingLogger();
13661388
const result = JsTyping.discoverTypings(host, logger.log, [app.path], getDirectoryPath(<Path>app.path), emptySafeList, cache, { enable: true }, ["http", "commander"]);
@@ -1433,12 +1455,22 @@ namespace ts.projectSystem {
14331455
path: "/a/package.json",
14341456
content: JSON.stringify({ dependencies: { commander: "1.0.0" } })
14351457
};
1458+
const packageLockFile = {
1459+
path: "/a/cache/package-lock.json",
1460+
content: JSON.stringify({
1461+
dependencies: {
1462+
"@types/commander": {
1463+
version: "1.0.0"
1464+
}
1465+
}
1466+
})
1467+
};
14361468
const cachePath = "/a/cache/";
14371469
const commander = {
14381470
path: cachePath + "node_modules/@types/commander/index.d.ts",
14391471
content: "export let x: number"
14401472
};
1441-
const host = createServerHost([f1, packageFile]);
1473+
const host = createServerHost([f1, packageFile, packageLockFile]);
14421474
let beginEvent: server.BeginInstallTypes;
14431475
let endEvent: server.EndInstallTypes;
14441476
const installer = new (class extends Installer {

src/server/server.ts

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

257257
// This number is essentially arbitrary. Processing more than one typings request
258258
// 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
@@ -77,7 +77,7 @@ declare namespace ts.server {
7777
/* @internal */
7878
export interface TypesRegistryResponse extends TypingInstallerResponse {
7979
readonly kind: EventTypesRegistry;
80-
readonly typesRegistry: MapLike<void>;
80+
readonly typesRegistry: MapLike<MapLike<string>>;
8181
}
8282

8383
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: 26 additions & 9 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;
@@ -104,7 +109,7 @@ namespace ts.server.typingsInstaller {
104109
private installRunCount = 1;
105110
private inFlightRequestCount = 0;
106111

107-
abstract readonly typesRegistry: Map<void>;
112+
abstract readonly typesRegistry: Map<MapLike<string>>;
108113

109114
constructor(
110115
protected readonly installTypingHost: InstallTypingHost,
@@ -217,15 +222,18 @@ namespace ts.server.typingsInstaller {
217222
}
218223
const typeDeclarationTimestamps = loadTypeDeclarationTimestampFile(timestampsFilePath || combinePaths(cacheLocation, timestampsFileName), this.installTypingHost, this.log);
219224
const packageJson = combinePaths(cacheLocation, "package.json");
225+
const packageLockJson = combinePaths(cacheLocation, "package-lock.json");
220226
if (this.log.isEnabled()) {
221227
this.log.writeLine(`Trying to find '${packageJson}'...`);
222228
}
223-
if (this.installTypingHost.fileExists(packageJson)) {
229+
if (this.installTypingHost.fileExists(packageJson) && this.installTypingHost.fileExists(packageLockJson)) {
224230
const npmConfig = <NpmConfig>JSON.parse(this.installTypingHost.readFile(packageJson));
231+
const npmLock = <NpmLock>JSON.parse(this.installTypingHost.readFile(packageLockJson));
225232
if (this.log.isEnabled()) {
226233
this.log.writeLine(`Loaded content of '${packageJson}': ${JSON.stringify(npmConfig)}`);
234+
this.log.writeLine(`Loaded content of '${packageLockJson}'`);
227235
}
228-
if (npmConfig.devDependencies) {
236+
if (npmConfig.devDependencies && npmLock.dependencies) {
229237
for (const key in npmConfig.devDependencies) {
230238
// key is @types/<package name>
231239
const packageName = getBaseFileName(key);
@@ -259,8 +267,11 @@ namespace ts.server.typingsInstaller {
259267
this.log.writeLine(`Adding entry into timestamp cache: '${key}' => '${timestamp}'`);
260268
}
261269
}
270+
const info = getProperty(npmLock.dependencies, key);
271+
const version = info && info.version;
272+
const semver = Semver.parse(version);
262273
// timestamp guaranteed to not be undefined by above check
263-
const newTyping: JsTyping.CachedTyping = { typingLocation: typingFile, timestamp: getProperty(typeDeclarationTimestamps, key) };
274+
const newTyping: JsTyping.CachedTyping = { typingLocation: typingFile, timestamp: getProperty(typeDeclarationTimestamps, key), version: semver };
264275
this.packageNameToTypingLocation.set(packageName, newTyping);
265276
}
266277
}
@@ -277,10 +288,6 @@ namespace ts.server.typingsInstaller {
277288
if (this.log.isEnabled()) this.log.writeLine(`'${typing}' is in missingTypingsSet - skipping...`);
278289
return false;
279290
}
280-
if (this.packageNameToTypingLocation.get(typing) && !JsTyping.isTypingExpired(this.packageNameToTypingLocation.get(typing))) {
281-
if (this.log.isEnabled()) this.log.writeLine(`'${typing}' already has a typing - skipping...`);
282-
return false;
283-
}
284291
const validationResult = JsTyping.validatePackageName(typing);
285292
if (validationResult !== JsTyping.PackageNameValidationResult.Ok) {
286293
// add typing name to missing set so we won't process it again
@@ -292,8 +299,17 @@ namespace ts.server.typingsInstaller {
292299
if (this.log.isEnabled()) this.log.writeLine(`Entry for package '${typing}' does not exist in local types registry - skipping...`);
293300
return false;
294301
}
302+
if (this.packageNameToTypingLocation.get(typing) && isTypingUpToDate(this.packageNameToTypingLocation.get(typing), this.typesRegistry.get(typing))) {
303+
if (this.log.isEnabled()) this.log.writeLine(`'${typing}' already has an up-to-date typing - skipping...`);
304+
return false;
305+
}
295306
return true;
296307
});
308+
309+
function isTypingUpToDate(cachedTyping: JsTyping.CachedTyping, availableTypingVersions: MapLike<string>) {
310+
const availableVersion = Semver.parse(getProperty(availableTypingVersions, `ts${ts.version}`));
311+
return !availableVersion.greaterThan(cachedTyping.version);
312+
}
297313
}
298314

299315
protected ensurePackageDirectoryExists(directory: string) {
@@ -364,7 +380,8 @@ namespace ts.server.typingsInstaller {
364380
}
365381

366382
const newTimestamp = Date.now();
367-
const newTyping: JsTyping.CachedTyping = { typingLocation: typingFile, timestamp: newTimestamp };
383+
const newVersion = Semver.parse(this.typesRegistry.get(packageName)[`ts${ts.versionMajorMinor}`]);
384+
const newTyping: JsTyping.CachedTyping = { typingLocation: typingFile, timestamp: newTimestamp, version: newVersion };
368385
this.packageNameToTypingLocation.set(packageName, newTyping);
369386
typeDeclarationTimestamps[typesPackageName(packageName)] = newTimestamp;
370387
installedTypingFiles.push(typingFile);

src/services/jsTyping.ts

Lines changed: 3 additions & 1 deletion
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 {
@@ -29,6 +30,7 @@ namespace ts.JsTyping {
2930
export interface CachedTyping {
3031
typingLocation: string;
3132
timestamp: number;
33+
version: Semver;
3234
}
3335

3436
export function isTypingExpired(typing: JsTyping.CachedTyping | undefined) {
@@ -70,7 +72,7 @@ namespace ts.JsTyping {
7072
* @param fileNames are the file names that belong to the same project
7173
* @param projectRootPath is the path to the project root directory
7274
* @param safeListPath is the path used to retrieve the safe list
73-
* @param packageNameToTypingLocation is the map of package names to their cached typing locations and time of caching
75+
* @param packageNameToTypingLocation is the map of package names to their cached typing locations and time of caching and versions
7476
* @param typeAcquisition is used to customize the typing acquisition process
7577
* @param compilerOptions are used as a source for typing inference
7678
*/

src/services/semver.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/* @internal */
2+
namespace ts {
3+
function intOfString(str: string): number {
4+
const n = parseInt(str, 10);
5+
if (isNaN(n)) {
6+
throw new Error(`Error in parseInt(${JSON.stringify(str)})`);
7+
}
8+
return n;
9+
}
10+
11+
export class Semver {
12+
static parse(semver: string): Semver {
13+
const isPrerelease = /^(.*)-next.\d+/.test(semver);
14+
const result = Semver.tryParse(semver, isPrerelease);
15+
if (!result) {
16+
throw new Error(`Unexpected semver: ${semver} (isPrerelease: ${isPrerelease})`);
17+
}
18+
return result;
19+
}
20+
21+
static fromRaw({ major, minor, patch, isPrerelease }: Semver): Semver {
22+
return new Semver(major, minor, patch, isPrerelease);
23+
}
24+
25+
// This must parse the output of `versionString`.
26+
static tryParse(semver: string, isPrerelease: boolean): Semver | undefined {
27+
// Per the semver spec <http://semver.org/#spec-item-2>:
28+
// "A normal version number MUST take the form X.Y.Z where X, Y, and Z are non-negative integers, and MUST NOT contain leading zeroes."
29+
const rgx = isPrerelease ? /^(\d+)\.(\d+)\.0-next.(\d+)$/ : /^(\d+)\.(\d+)\.(\d+)$/;
30+
const match = rgx.exec(semver);
31+
return match ? new Semver(intOfString(match[1]), intOfString(match[2]), intOfString(match[3]), isPrerelease) : undefined;
32+
}
33+
34+
constructor(
35+
readonly major: number, readonly minor: number, readonly patch: number,
36+
/**
37+
* If true, this is `major.minor.0-next.patch`.
38+
* If false, this is `major.minor.patch`.
39+
*/
40+
readonly isPrerelease: boolean) { }
41+
42+
get versionString(): string {
43+
return this.isPrerelease ? `${this.major}.${this.minor}.0-next.${this.patch}` : `${this.major}.${this.minor}.${this.patch}`;
44+
}
45+
46+
equals(sem: Semver): boolean {
47+
return this.major === sem.major && this.minor === sem.minor && this.patch === sem.patch && this.isPrerelease === sem.isPrerelease;
48+
}
49+
50+
greaterThan(sem: Semver): boolean {
51+
return this.major > sem.major || this.major === sem.major
52+
&& (this.minor > sem.minor || this.minor === sem.minor && this.patch > sem.patch);
53+
}
54+
}
55+
}

src/services/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"services.ts",
6262
"transform.ts",
6363
"transpile.ts",
64+
"semver.ts",
6465
"shims.ts",
6566
"signatureHelp.ts",
6667
"symbolDisplay.ts",

0 commit comments

Comments
 (0)