TypeScript/src/server/typingsInstaller/typingsInstaller.ts

290 lines
12 KiB
TypeScript
Raw Normal View History

2016-08-14 20:16:11 +02:00
/// <reference path="../../services/jsTyping.ts"/>
/// <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);
interface TsdConfig {
installed: MapLike<any>;
}
export interface Log {
isEnabled(): boolean;
writeLine(text: string): void;
}
const nullLog: Log = {
isEnabled: () => false,
writeLine: () => {}
}
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;
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[]>();
abstract readonly installTypingHost: InstallTypingHost;
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-12 20:04:43 +02:00
init() {
this.isTsdInstalled = this.isPackageInstalled("tsd");
if (this.log.isEnabled()) {
this.log.writeLine(`isTsdInstalled: ${this.isTsdInstalled}`);
}
2016-08-12 20:04:43 +02:00
if (!this.isTsdInstalled) {
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");
if (this.log.isEnabled()) {
this.log.writeLine(`isTsdInstalled: ${this.isTsdInstalled}`);
}
2016-08-12 20:04:43 +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) {
if (this.log.isEnabled()) {
this.log.writeLine(`tsd is not installed, ignoring request...`);
}
2016-08-12 20:04:43 +02:00
return;
}
if (this.log.isEnabled()) {
this.log.writeLine(`Got install request ${JSON.stringify(req)}`);
}
2016-08-12 20:04:43 +02:00
// load existing typing information from the cache
if (req.cachePath) {
if (this.log.isEnabled()) {
this.log.writeLine(`Request specifies cache path '${req.cachePath}', loading cached information...`);
}
this.processCacheLocation(req.cachePath);
}
2016-08-12 20:04:43 +02:00
const discoverTypingsResult = JsTyping.discoverTypings(
this.installTypingHost,
2016-08-12 20:04:43 +02:00
req.fileNames,
req.projectRootPath,
this.safeListPath,
this.packageNameToTypingLocation,
2016-08-12 20:04:43 +02:00
req.typingOptions,
req.compilerOptions);
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-12 21:14:25 +02:00
// start watching files
this.watchFiles(req.projectName, discoverTypingsResult.filesToWatch);
// install typings
this.installTypings(req, req.cachePath || this.globalCachePath, discoverTypingsResult.cachedTypingPaths, discoverTypingsResult.newTypingNames);
2016-08-12 20:04:43 +02:00
}
private processCacheLocation(cacheLocation: string) {
if (this.log.isEnabled()) {
this.log.writeLine(`Processing cache location '${cacheLocation}'`);
}
if (this.knownCachesSet[cacheLocation]) {
if (this.log.isEnabled()) {
this.log.writeLine(`Cache location was already processed...`)
}
return;
}
const tsdJson = combinePaths(cacheLocation, "tsd.json");
if (this.log.isEnabled()) {
this.log.writeLine(`Trying to find '${tsdJson}'...`);
}
if (this.installTypingHost.fileExists(tsdJson)) {
const tsdConfig = <TsdConfig>JSON.parse(this.installTypingHost.readFile(tsdJson));
if (this.log.isEnabled()) {
this.log.writeLine(`Loaded content of '${tsdJson}': ${JSON.stringify(tsdConfig)}`);
}
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) {
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}'`);
}
this.packageNameToTypingLocation[packageName] = typingFile;
}
}
}
if (this.log.isEnabled()) {
this.log.writeLine(`Finished processing cache location '${cacheLocation}'`);
}
this.knownCachesSet[cacheLocation] = true;
}
2016-08-16 23:21:09 +02:00
private installTypings(req: DiscoverTypings, cachePath: string, currentlyCachedTypings: string[], typingsToInstall: string[]) {
if (this.log.isEnabled()) {
this.log.writeLine(`Installing typings ${JSON.stringify(typingsToInstall)}`);
}
typingsToInstall = filter(typingsToInstall, x => !this.missingTypingsSet[x]);
2016-08-12 21:14:25 +02:00
if (typingsToInstall.length === 0) {
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
const tsdPath = combinePaths(cachePath, "tsd.json");
if (this.log.isEnabled()) {
this.log.writeLine(`Tsd config file: ${tsdPath}`);
}
if (!this.installTypingHost.fileExists(tsdPath)) {
if (this.log.isEnabled()) {
this.log.writeLine(`Tsd config file '${tsdPath}' is missing, creating new one...`);
}
this.ensureDirectoryExists(cachePath, this.installTypingHost);
this.installTypingHost.writeFile(tsdPath, DefaultTsdSettings);
2016-08-12 20:04:43 +02:00
}
2016-08-12 21:14:25 +02:00
this.runTsd(cachePath, typingsToInstall, installedTypings => {
2016-08-12 21:14:25 +02:00
// TODO: watch project directory
if (this.log.isEnabled()) {
this.log.writeLine(`Requested to install typings ${JSON.stringify(typingsToInstall)}, installed typings ${JSON.stringify(installedTypings)}`);
}
const installedPackages: Map<true> = createMap<true>();
const installedTypingFiles: string[] = [];
for (const t of installedTypings) {
const packageName = getPackageName(t);
if (!packageName) {
continue;
}
installedPackages[packageName] = true;
installedTypingFiles.push(tsdTypingToFileName(cachePath, t));
}
if (this.log.isEnabled()) {
this.log.writeLine(`Installed typing files ${JSON.stringify(installedTypingFiles)}`);
}
for (const toInstall of typingsToInstall) {
if (!installedPackages[toInstall]) {
if (this.log.isEnabled()) {
this.log.writeLine(`New missing typing package '${toInstall}'`);
}
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);
}
}
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()) {
this.log.writeLine(`FS notification for '${f}', sending 'clean' message for project '${projectName}'`);
2016-08-16 23:21:09 +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
}
}