2016-08-14 20:16:11 +02:00
|
|
|
/// <reference path="../../services/jsTyping.ts"/>
|
2016-08-12 23:01:23 +02:00
|
|
|
/// <reference path="../types.d.ts"/>
|
2016-08-12 20:04:43 +02:00
|
|
|
|
|
|
|
namespace ts.server.typingsInstaller {
|
|
|
|
const DefaultTsdSettings = JSON.stringify({
|
|
|
|
version: "v4",
|
|
|
|
repo: "DefinitelyTyped/DefinitelyTyped",
|
|
|
|
ref: "master",
|
|
|
|
path: "typings"
|
|
|
|
}, /*replacer*/undefined, /*space*/4);
|
|
|
|
|
2016-08-15 20:48:28 +02:00
|
|
|
interface TsdConfig {
|
|
|
|
installed: MapLike<any>;
|
|
|
|
}
|
|
|
|
|
2016-08-16 00:59:31 +02:00
|
|
|
export interface Log {
|
|
|
|
isEnabled(): boolean;
|
|
|
|
writeLine(text: string): void;
|
|
|
|
}
|
|
|
|
|
|
|
|
const nullLog: Log = {
|
|
|
|
isEnabled: () => false,
|
|
|
|
writeLine: () => {}
|
|
|
|
}
|
|
|
|
|
2016-08-15 20:48:28 +02:00
|
|
|
function tsdTypingToFileName(cachePath: string, tsdTypingFile: string) {
|
|
|
|
return combinePaths(cachePath, `typings/${tsdTypingFile}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
function getPackageName(tsdTypingFile: string) {
|
|
|
|
const idx = tsdTypingFile.indexOf("/");
|
|
|
|
return idx > 0 ? tsdTypingFile.substr(0, idx) : undefined;
|
|
|
|
}
|
|
|
|
|
2016-08-12 20:04:43 +02:00
|
|
|
export abstract class TypingsInstaller {
|
|
|
|
|
|
|
|
private isTsdInstalled: boolean;
|
2016-08-15 20:48:28 +02:00
|
|
|
|
|
|
|
private packageNameToTypingLocation: Map<string> = createMap<string>();
|
|
|
|
private missingTypingsSet: Map<true> = createMap<true>();
|
|
|
|
private knownCachesSet: Map<true> = createMap<true>();
|
|
|
|
|
2016-08-16 23:21:09 +02:00
|
|
|
private projectWatchers: Map<FileWatcher[]> = createMap<FileWatcher[]>();
|
|
|
|
|
2016-08-15 20:48:28 +02:00
|
|
|
abstract readonly installTypingHost: InstallTypingHost;
|
|
|
|
|
2016-08-16 00:59:31 +02:00
|
|
|
constructor(readonly globalCachePath: string, readonly safeListPath: Path, protected readonly log = nullLog) {
|
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`Global cache location '${globalCachePath}', safe file path '${safeListPath}'`);
|
|
|
|
}
|
2016-08-15 20:48:28 +02:00
|
|
|
}
|
2016-08-12 20:04:43 +02:00
|
|
|
|
|
|
|
init() {
|
|
|
|
this.isTsdInstalled = this.isPackageInstalled("tsd");
|
2016-08-16 00:59:31 +02:00
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`isTsdInstalled: ${this.isTsdInstalled}`);
|
|
|
|
}
|
|
|
|
|
2016-08-12 20:04:43 +02:00
|
|
|
if (!this.isTsdInstalled) {
|
2016-08-16 00:59:31 +02:00
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`tsd is not installed, installing tsd...`);
|
|
|
|
}
|
2016-08-12 20:04:43 +02:00
|
|
|
this.isTsdInstalled = this.installPackage("tsd");
|
2016-08-16 00:59:31 +02:00
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`isTsdInstalled: ${this.isTsdInstalled}`);
|
|
|
|
}
|
2016-08-12 20:04:43 +02:00
|
|
|
}
|
2016-08-15 20:48:28 +02:00
|
|
|
this.processCacheLocation(this.globalCachePath);
|
2016-08-12 20:04:43 +02:00
|
|
|
}
|
|
|
|
|
2016-08-16 23:21:09 +02:00
|
|
|
closeProject(req: CloseProject) {
|
|
|
|
this.closeWatchers(req.projectName);
|
|
|
|
}
|
|
|
|
|
|
|
|
private closeWatchers(projectName: string): boolean {
|
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`Closing file watchers for project '${projectName}'`);
|
|
|
|
}
|
|
|
|
const watchers = this.projectWatchers[projectName];
|
|
|
|
if (!watchers) {
|
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`No watchers are registered for project '${projectName}'`);
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
for (const w of watchers) {
|
|
|
|
w.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
delete this.projectWatchers[projectName]
|
|
|
|
|
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`Closing file watchers for project '${projectName}' - done.`);
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
install(req: DiscoverTypings) {
|
2016-08-12 20:04:43 +02:00
|
|
|
if (!this.isTsdInstalled) {
|
2016-08-16 00:59:31 +02:00
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`tsd is not installed, ignoring request...`);
|
|
|
|
}
|
2016-08-12 20:04:43 +02:00
|
|
|
return;
|
|
|
|
}
|
2016-08-16 00:59:31 +02:00
|
|
|
|
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`Got install request ${JSON.stringify(req)}`);
|
|
|
|
}
|
2016-08-12 20:04:43 +02:00
|
|
|
|
2016-08-15 20:48:28 +02:00
|
|
|
// load existing typing information from the cache
|
|
|
|
if (req.cachePath) {
|
2016-08-16 00:59:31 +02:00
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`Request specifies cache path '${req.cachePath}', loading cached information...`);
|
|
|
|
}
|
2016-08-15 20:48:28 +02:00
|
|
|
this.processCacheLocation(req.cachePath);
|
|
|
|
}
|
|
|
|
|
2016-08-12 20:04:43 +02:00
|
|
|
const discoverTypingsResult = JsTyping.discoverTypings(
|
2016-08-15 20:48:28 +02:00
|
|
|
this.installTypingHost,
|
2016-08-12 20:04:43 +02:00
|
|
|
req.fileNames,
|
|
|
|
req.projectRootPath,
|
2016-08-15 20:48:28 +02:00
|
|
|
this.safeListPath,
|
|
|
|
this.packageNameToTypingLocation,
|
2016-08-12 20:04:43 +02:00
|
|
|
req.typingOptions,
|
|
|
|
req.compilerOptions);
|
2016-08-16 00:59:31 +02:00
|
|
|
|
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`Finished typings discovery: ${JSON.stringify(discoverTypingsResult)}`);
|
|
|
|
}
|
2016-08-12 20:04:43 +02:00
|
|
|
|
|
|
|
// respond with whatever cached typings we have now
|
2016-08-16 23:21:09 +02:00
|
|
|
this.sendResponse(this.createSetTypings(req, discoverTypingsResult.cachedTypingPaths));
|
2016-08-15 20:48:28 +02:00
|
|
|
|
2016-08-12 21:14:25 +02:00
|
|
|
// start watching files
|
2016-08-16 23:49:45 +02:00
|
|
|
this.watchFiles(req.projectName, discoverTypingsResult.filesToWatch);
|
2016-08-15 20:48:28 +02:00
|
|
|
|
2016-08-16 00:59:31 +02:00
|
|
|
// install typings
|
|
|
|
this.installTypings(req, req.cachePath || this.globalCachePath, discoverTypingsResult.cachedTypingPaths, discoverTypingsResult.newTypingNames);
|
2016-08-12 20:04:43 +02:00
|
|
|
}
|
|
|
|
|
2016-08-15 20:48:28 +02:00
|
|
|
private processCacheLocation(cacheLocation: string) {
|
2016-08-16 00:59:31 +02:00
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`Processing cache location '${cacheLocation}'`);
|
|
|
|
}
|
2016-08-15 20:48:28 +02:00
|
|
|
if (this.knownCachesSet[cacheLocation]) {
|
2016-08-16 00:59:31 +02:00
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`Cache location was already processed...`)
|
|
|
|
}
|
2016-08-15 20:48:28 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
const tsdJson = combinePaths(cacheLocation, "tsd.json");
|
2016-08-16 00:59:31 +02:00
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`Trying to find '${tsdJson}'...`);
|
|
|
|
}
|
2016-08-15 20:48:28 +02:00
|
|
|
if (this.installTypingHost.fileExists(tsdJson)) {
|
|
|
|
const tsdConfig = <TsdConfig>JSON.parse(this.installTypingHost.readFile(tsdJson));
|
2016-08-16 00:59:31 +02:00
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`Loaded content of '${tsdJson}': ${JSON.stringify(tsdConfig)}`);
|
|
|
|
}
|
2016-08-15 20:48:28 +02:00
|
|
|
if (tsdConfig.installed) {
|
|
|
|
for (const key in tsdConfig.installed) {
|
|
|
|
// key is <package name>/<typing file>
|
|
|
|
const packageName = getPackageName(key);
|
|
|
|
if (!packageName) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const typingFile = tsdTypingToFileName(cacheLocation, key);
|
|
|
|
const existingTypingFile = this.packageNameToTypingLocation[packageName];
|
|
|
|
if (existingTypingFile === typingFile) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (existingTypingFile) {
|
2016-08-16 00:59:31 +02:00
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`New typing for package ${packageName} from '${typingFile}' conflicts with existing typing file '${existingTypingFile}'`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`Adding entry into typings cache: '${packageName}' => '${typingFile}'`);
|
2016-08-15 20:48:28 +02:00
|
|
|
}
|
|
|
|
this.packageNameToTypingLocation[packageName] = typingFile;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-08-16 00:59:31 +02:00
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`Finished processing cache location '${cacheLocation}'`);
|
|
|
|
}
|
2016-08-15 20:48:28 +02:00
|
|
|
this.knownCachesSet[cacheLocation] = true;
|
|
|
|
}
|
|
|
|
|
2016-08-16 23:21:09 +02:00
|
|
|
private installTypings(req: DiscoverTypings, cachePath: string, currentlyCachedTypings: string[], typingsToInstall: string[]) {
|
2016-08-16 00:59:31 +02:00
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`Installing typings ${JSON.stringify(typingsToInstall)}`);
|
|
|
|
}
|
2016-08-15 20:48:28 +02:00
|
|
|
typingsToInstall = filter(typingsToInstall, x => !this.missingTypingsSet[x]);
|
2016-08-12 21:14:25 +02:00
|
|
|
if (typingsToInstall.length === 0) {
|
2016-08-16 00:59:31 +02:00
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`All typings are known to be missing - no need to go any further`);
|
|
|
|
}
|
2016-08-12 21:14:25 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-08-12 20:04:43 +02:00
|
|
|
// TODO: install typings and send response when they are ready
|
2016-08-16 00:59:31 +02:00
|
|
|
const tsdPath = combinePaths(cachePath, "tsd.json");
|
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`Tsd config file: ${tsdPath}`);
|
|
|
|
}
|
2016-08-15 20:48:28 +02:00
|
|
|
if (!this.installTypingHost.fileExists(tsdPath)) {
|
2016-08-16 00:59:31 +02:00
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`Tsd config file '${tsdPath}' is missing, creating new one...`);
|
|
|
|
}
|
|
|
|
this.ensureDirectoryExists(cachePath, this.installTypingHost);
|
2016-08-15 20:48:28 +02:00
|
|
|
this.installTypingHost.writeFile(tsdPath, DefaultTsdSettings);
|
2016-08-12 20:04:43 +02:00
|
|
|
}
|
2016-08-12 21:14:25 +02:00
|
|
|
|
2016-08-16 00:59:31 +02:00
|
|
|
this.runTsd(cachePath, typingsToInstall, installedTypings => {
|
2016-08-12 21:14:25 +02:00
|
|
|
// TODO: watch project directory
|
2016-08-16 00:59:31 +02:00
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`Requested to install typings ${JSON.stringify(typingsToInstall)}, installed typings ${JSON.stringify(installedTypings)}`);
|
|
|
|
}
|
2016-08-15 20:48:28 +02:00
|
|
|
const installedPackages: Map<true> = createMap<true>();
|
|
|
|
const installedTypingFiles: string[] = [];
|
|
|
|
for (const t of installedTypings) {
|
|
|
|
const packageName = getPackageName(t);
|
|
|
|
if (!packageName) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
installedPackages[packageName] = true;
|
2016-08-16 00:59:31 +02:00
|
|
|
installedTypingFiles.push(tsdTypingToFileName(cachePath, t));
|
|
|
|
}
|
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`Installed typing files ${JSON.stringify(installedTypingFiles)}`);
|
2016-08-15 20:48:28 +02:00
|
|
|
}
|
|
|
|
for (const toInstall of typingsToInstall) {
|
|
|
|
if (!installedPackages[toInstall]) {
|
2016-08-16 00:59:31 +02:00
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`New missing typing package '${toInstall}'`);
|
|
|
|
}
|
2016-08-15 20:48:28 +02:00
|
|
|
this.missingTypingsSet[toInstall] = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-08-16 23:21:09 +02:00
|
|
|
this.sendResponse(this.createSetTypings(req, currentlyCachedTypings.concat(installedTypingFiles)));
|
2016-08-12 21:14:25 +02:00
|
|
|
});
|
2016-08-12 20:04:43 +02:00
|
|
|
}
|
|
|
|
|
2016-08-13 08:04:17 +02:00
|
|
|
private ensureDirectoryExists(directory: string, host: InstallTypingHost): void {
|
|
|
|
const directoryName = getDirectoryPath(directory);
|
|
|
|
if (!host.directoryExists(directoryName)) {
|
|
|
|
this.ensureDirectoryExists(directoryName, host);
|
|
|
|
}
|
|
|
|
if (!host.directoryExists(directory)) {
|
|
|
|
host.createDirectory(directory);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-08-16 23:49:45 +02:00
|
|
|
private watchFiles(projectName: string, files: string[]) {
|
2016-08-16 23:21:09 +02:00
|
|
|
if (!files.length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const watchers: FileWatcher[] = [];
|
|
|
|
for (const file of files) {
|
|
|
|
const w = this.installTypingHost.watchFile(file, f => {
|
|
|
|
if (this.log.isEnabled()) {
|
2016-08-16 23:49:45 +02:00
|
|
|
this.log.writeLine(`FS notification for '${f}', sending 'clean' message for project '${projectName}'`);
|
2016-08-16 23:21:09 +02:00
|
|
|
}
|
2016-08-16 23:49:45 +02:00
|
|
|
this.closeWatchers(projectName);
|
|
|
|
this.sendResponse(<InvalidateCachedTypings>{ projectName: projectName, kind: "invalidate" })
|
2016-08-16 23:21:09 +02:00
|
|
|
});
|
|
|
|
watchers.push(w);
|
|
|
|
}
|
2016-08-12 20:04:43 +02:00
|
|
|
}
|
|
|
|
|
2016-08-16 23:21:09 +02:00
|
|
|
private createSetTypings(request: DiscoverTypings, typings: string[]): SetTypings {
|
2016-08-12 20:04:43 +02:00
|
|
|
return {
|
|
|
|
projectName: request.projectName,
|
|
|
|
typingOptions: request.typingOptions,
|
|
|
|
compilerOptions: request.compilerOptions,
|
2016-08-16 23:21:09 +02:00
|
|
|
typings,
|
|
|
|
kind: "set"
|
2016-08-12 20:04:43 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
protected abstract isPackageInstalled(packageName: string): boolean;
|
|
|
|
protected abstract installPackage(packageName: string): boolean;
|
2016-08-16 23:21:09 +02:00
|
|
|
protected abstract sendResponse(response: SetTypings | InvalidateCachedTypings): void;
|
2016-08-12 21:14:25 +02:00
|
|
|
protected abstract runTsd(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void): void;
|
2016-08-12 20:04:43 +02:00
|
|
|
}
|
|
|
|
}
|