Update installed types if older than those listed in the registry

This commit is contained in:
uniqueiniquity 2018-01-11 09:13:33 -08:00
parent 23324345e2
commit e72ea6f7b1
9 changed files with 137 additions and 30 deletions

View File

@ -63,7 +63,7 @@ namespace ts.projectSystem {
readonly globalTypingsCacheLocation: string,
throttleLimit: number,
installTypingHost: server.ServerHost,
readonly typesRegistry = createMap<void>(),
readonly typesRegistry = createMap<MapLike<string>>(),
log?: TI.Log) {
super(installTypingHost, globalTypingsCacheLocation, safeList.path, customTypesMap.path, throttleLimit, log);
}

View File

@ -1,6 +1,7 @@
/// <reference path="../harness.ts" />
/// <reference path="./tsserverProjectSystem.ts" />
/// <reference path="../../server/typingsInstaller/typingsInstaller.ts" />
/// <reference path="../../services/semver.ts" />
namespace ts.projectSystem {
import TI = server.typingsInstaller;
@ -10,13 +11,24 @@ namespace ts.projectSystem {
interface InstallerParams {
globalTypingsCacheLocation?: string;
throttleLimit?: number;
typesRegistry?: Map<void>;
typesRegistry?: Map<MapLike<string>>;
}
function createTypesRegistry(...list: string[]): Map<void> {
const map = createMap<void>();
function createTypesRegistry(...list: string[]): Map<MapLike<string>> {
const versionMap = {
"latest": "1.3.0",
"ts2.0": "1.0.0",
"ts2.1": "1.0.0",
"ts2.2": "1.2.0",
"ts2.3": "1.3.0",
"ts2.4": "1.3.0",
"ts2.5": "1.3.0",
"ts2.6": "1.3.0",
"ts2.7": "1.3.0"
};
const map = createMap<MapLike<string>>();
for (const l of list) {
map.set(l, undefined);
map.set(l, versionMap);
}
return map;
}
@ -51,7 +63,7 @@ namespace ts.projectSystem {
const logs: string[] = [];
return {
log(message) {
logs.push(message);
logs.push(message);
},
finish() {
return logs;
@ -1149,7 +1161,17 @@ namespace ts.projectSystem {
"types-registry": "^0.1.317"
},
devDependencies: {
"@types/jquery": "^3.2.16"
"@types/jquery": "^1.3.0"
}
})
};
const cacheLockConfig = {
path: "/a/data/package-lock.json",
content: JSON.stringify({
dependencies: {
"@types/jquery": {
version: "1.3.0"
}
}
})
};
@ -1157,7 +1179,7 @@ namespace ts.projectSystem {
path: "/a/data/node_modules/@types/jquery/index.d.ts",
content: "declare const $: { x: number }"
};
const host = createServerHost([file1, packageJson, timestamps, cacheConfig, jquery]);
const host = createServerHost([file1, packageJson, timestamps, cacheConfig, cacheLockConfig, jquery]);
const installer = new (class extends Installer {
constructor() {
super(host, { typesRegistry: createTypesRegistry("jquery") });
@ -1299,7 +1321,7 @@ namespace ts.projectSystem {
content: ""
};
const host = createServerHost([f, node]);
const cache = createMapFromTemplate<JsTyping.CachedTyping>({ node: { typingLocation: node.path, timestamp: Date.now() } });
const cache = createMapFromTemplate<JsTyping.CachedTyping>({ node: { typingLocation: node.path, timestamp: Date.now(), version: new Semver(1, 0, 0, /*isPrerelease*/ false) } });
const logger = trackingLogger();
const result = JsTyping.discoverTypings(host, logger.log, [f.path], getDirectoryPath(<Path>f.path), emptySafeList, cache, { enable: true }, ["fs", "bar"]);
assert.deepEqual(logger.finish(), [
@ -1359,8 +1381,8 @@ namespace ts.projectSystem {
};
const host = createServerHost([app]);
const cache = createMapFromTemplate<JsTyping.CachedTyping>({
node: { typingLocation: node.path, timestamp: Date.now() },
commander: { typingLocation: commander.path, timestamp: date.getTime() }
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) }
});
const logger = trackingLogger();
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 {
path: "/a/package.json",
content: JSON.stringify({ dependencies: { commander: "1.0.0" } })
};
const packageLockFile = {
path: "/a/cache/package-lock.json",
content: JSON.stringify({
dependencies: {
"@types/commander": {
version: "1.0.0"
}
}
})
};
const cachePath = "/a/cache/";
const commander = {
path: cachePath + "node_modules/@types/commander/index.d.ts",
content: "export let x: number"
};
const host = createServerHost([f1, packageFile]);
const host = createServerHost([f1, packageFile, packageLockFile]);
let beginEvent: server.BeginInstallTypes;
let endEvent: server.EndInstallTypes;
const installer = new (class extends Installer {

View File

@ -252,7 +252,7 @@ namespace ts.server {
private requestMap = createMap<QueuedOperation>(); // Maps operation ID to newest requestQueue entry with that ID
/** We will lazily request the types registry on the first call to `isKnownTypesPackageName` and store it in `typesRegistryCache`. */
private requestedRegistry: boolean;
private typesRegistryCache: Map<void> | undefined;
private typesRegistryCache: Map<MapLike<string>> | undefined;
// This number is essentially arbitrary. Processing more than one typings request
// at a time makes sense, but having too many in the pipe results in a hang

View File

@ -77,7 +77,7 @@ declare namespace ts.server {
/* @internal */
export interface TypesRegistryResponse extends TypingInstallerResponse {
readonly kind: EventTypesRegistry;
readonly typesRegistry: MapLike<void>;
readonly typesRegistry: MapLike<MapLike<string>>;
}
export interface PackageInstalledResponse extends ProjectResponse {

View File

@ -41,15 +41,15 @@ namespace ts.server.typingsInstaller {
}
interface TypesRegistryFile {
entries: MapLike<void>;
entries: MapLike<MapLike<string>>;
}
function loadTypesRegistryFile(typesRegistryFilePath: string, host: InstallTypingHost, log: Log): Map<void> {
function loadTypesRegistryFile(typesRegistryFilePath: string, host: InstallTypingHost, log: Log): Map<MapLike<string>> {
if (!host.fileExists(typesRegistryFilePath)) {
if (log.isEnabled()) {
log.writeLine(`Types registry file '${typesRegistryFilePath}' does not exist`);
}
return createMap<void>();
return createMap<MapLike<string>>();
}
try {
const content = <TypesRegistryFile>JSON.parse(host.readFile(typesRegistryFilePath));
@ -59,7 +59,7 @@ namespace ts.server.typingsInstaller {
if (log.isEnabled()) {
log.writeLine(`Error when loading types registry file '${typesRegistryFilePath}': ${(<Error>e).message}, ${(<Error>e).stack}`);
}
return createMap<void>();
return createMap<MapLike<string>>();
}
}
@ -77,7 +77,7 @@ namespace ts.server.typingsInstaller {
export class NodeTypingsInstaller extends TypingsInstaller {
private readonly nodeExecSync: ExecSync;
private readonly npmPath: string;
readonly typesRegistry: Map<void>;
readonly typesRegistry: Map<MapLike<string>>;
private delayedInitializationError: InitializationFailedResponse | undefined;
@ -141,7 +141,7 @@ namespace ts.server.typingsInstaller {
this.closeProject(req);
break;
case "typesRegistry": {
const typesRegistry: { [key: string]: void } = {};
const typesRegistry: { [key: string]: MapLike<string> } = {};
this.typesRegistry.forEach((value, key) => {
typesRegistry[key] = value;
});

View File

@ -1,6 +1,7 @@
/// <reference path="../../compiler/core.ts" />
/// <reference path="../../compiler/moduleNameResolver.ts" />
/// <reference path="../../services/jsTyping.ts"/>
/// <reference path="../../services/semver.ts"/>
/// <reference path="../types.ts"/>
/// <reference path="../shared.ts"/>
@ -9,6 +10,10 @@ namespace ts.server.typingsInstaller {
devDependencies: MapLike<any>;
}
interface NpmLock {
dependencies: { [packageName: string]: { version: string } };
}
export interface Log {
isEnabled(): boolean;
writeLine(text: string): void;
@ -104,7 +109,7 @@ namespace ts.server.typingsInstaller {
private installRunCount = 1;
private inFlightRequestCount = 0;
abstract readonly typesRegistry: Map<void>;
abstract readonly typesRegistry: Map<MapLike<string>>;
constructor(
protected readonly installTypingHost: InstallTypingHost,
@ -217,15 +222,18 @@ namespace ts.server.typingsInstaller {
}
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()) {
this.log.writeLine(`Trying to find '${packageJson}'...`);
}
if (this.installTypingHost.fileExists(packageJson)) {
if (this.installTypingHost.fileExists(packageJson) && this.installTypingHost.fileExists(packageLockJson)) {
const npmConfig = <NpmConfig>JSON.parse(this.installTypingHost.readFile(packageJson));
const npmLock = <NpmLock>JSON.parse(this.installTypingHost.readFile(packageLockJson));
if (this.log.isEnabled()) {
this.log.writeLine(`Loaded content of '${packageJson}': ${JSON.stringify(npmConfig)}`);
this.log.writeLine(`Loaded content of '${packageLockJson}'`);
}
if (npmConfig.devDependencies) {
if (npmConfig.devDependencies && npmLock.dependencies) {
for (const key in npmConfig.devDependencies) {
// key is @types/<package name>
const packageName = getBaseFileName(key);
@ -259,8 +267,11 @@ namespace ts.server.typingsInstaller {
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) };
const newTyping: JsTyping.CachedTyping = { typingLocation: typingFile, timestamp: getProperty(typeDeclarationTimestamps, key), version: semver };
this.packageNameToTypingLocation.set(packageName, newTyping);
}
}
@ -277,10 +288,6 @@ namespace ts.server.typingsInstaller {
if (this.log.isEnabled()) this.log.writeLine(`'${typing}' is in missingTypingsSet - skipping...`);
return false;
}
if (this.packageNameToTypingLocation.get(typing) && !JsTyping.isTypingExpired(this.packageNameToTypingLocation.get(typing))) {
if (this.log.isEnabled()) this.log.writeLine(`'${typing}' already has a typing - skipping...`);
return false;
}
const validationResult = JsTyping.validatePackageName(typing);
if (validationResult !== JsTyping.PackageNameValidationResult.Ok) {
// add typing name to missing set so we won't process it again
@ -292,8 +299,17 @@ 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.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<string>) {
const availableVersion = Semver.parse(getProperty(availableTypingVersions, `ts${ts.version}`));
return !availableVersion.greaterThan(cachedTyping.version);
}
}
protected ensurePackageDirectoryExists(directory: string) {
@ -364,7 +380,8 @@ namespace ts.server.typingsInstaller {
}
const newTimestamp = Date.now();
const newTyping: JsTyping.CachedTyping = { typingLocation: typingFile, timestamp: newTimestamp };
const newVersion = Semver.parse(this.typesRegistry.get(packageName)[`ts${ts.versionMajorMinor}`]);
const newTyping: JsTyping.CachedTyping = { typingLocation: typingFile, timestamp: newTimestamp, version: newVersion };
this.packageNameToTypingLocation.set(packageName, newTyping);
typeDeclarationTimestamps[typesPackageName(packageName)] = newTimestamp;
installedTypingFiles.push(typingFile);

View File

@ -4,6 +4,7 @@
/// <reference path='../compiler/types.ts' />
/// <reference path='../compiler/core.ts' />
/// <reference path='../compiler/commandLineParser.ts' />
/// <reference path='../services/semver.ts' />
/* @internal */
namespace ts.JsTyping {
@ -29,6 +30,7 @@ namespace ts.JsTyping {
export interface CachedTyping {
typingLocation: string;
timestamp: number;
version: Semver;
}
export function isTypingExpired(typing: JsTyping.CachedTyping | undefined) {
@ -70,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
* @param packageNameToTypingLocation is the map of package names to their cached typing locations and time of caching and versions
* @param typeAcquisition is used to customize the typing acquisition process
* @param compilerOptions are used as a source for typing inference
*/

55
src/services/semver.ts Normal file
View File

@ -0,0 +1,55 @@
/* @internal */
namespace ts {
function intOfString(str: string): number {
const n = parseInt(str, 10);
if (isNaN(n)) {
throw new Error(`Error in parseInt(${JSON.stringify(str)})`);
}
return n;
}
export class Semver {
static parse(semver: string): Semver {
const isPrerelease = /^(.*)-next.\d+/.test(semver);
const result = Semver.tryParse(semver, isPrerelease);
if (!result) {
throw new Error(`Unexpected semver: ${semver} (isPrerelease: ${isPrerelease})`);
}
return result;
}
static fromRaw({ major, minor, patch, isPrerelease }: Semver): Semver {
return new Semver(major, minor, patch, isPrerelease);
}
// This must parse the output of `versionString`.
static tryParse(semver: string, isPrerelease: boolean): Semver | undefined {
// Per the semver spec <http://semver.org/#spec-item-2>:
// "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."
const rgx = isPrerelease ? /^(\d+)\.(\d+)\.0-next.(\d+)$/ : /^(\d+)\.(\d+)\.(\d+)$/;
const match = rgx.exec(semver);
return match ? new Semver(intOfString(match[1]), intOfString(match[2]), intOfString(match[3]), isPrerelease) : undefined;
}
constructor(
readonly major: number, readonly minor: number, readonly patch: number,
/**
* If true, this is `major.minor.0-next.patch`.
* If false, this is `major.minor.patch`.
*/
readonly isPrerelease: boolean) { }
get versionString(): string {
return this.isPrerelease ? `${this.major}.${this.minor}.0-next.${this.patch}` : `${this.major}.${this.minor}.${this.patch}`;
}
equals(sem: Semver): boolean {
return this.major === sem.major && this.minor === sem.minor && this.patch === sem.patch && this.isPrerelease === sem.isPrerelease;
}
greaterThan(sem: Semver): boolean {
return this.major > sem.major || this.major === sem.major
&& (this.minor > sem.minor || this.minor === sem.minor && this.patch > sem.patch);
}
}
}

View File

@ -61,6 +61,7 @@
"services.ts",
"transform.ts",
"transpile.ts",
"semver.ts",
"shims.ts",
"signatureHelp.ts",
"symbolDisplay.ts",