2016-09-19 22:56:30 +02:00
|
|
|
|
/// <reference path="../../compiler/core.ts" />
|
2016-09-08 22:26:01 +02:00
|
|
|
|
/// <reference path="../../compiler/moduleNameResolver.ts" />
|
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 {
|
2016-08-26 01:25:34 +02:00
|
|
|
|
interface NpmConfig {
|
|
|
|
|
devDependencies: MapLike<any>;
|
2016-08-15 20:48:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
2016-08-16 00:59:31 +02:00
|
|
|
|
export interface Log {
|
|
|
|
|
isEnabled(): boolean;
|
|
|
|
|
writeLine(text: string): void;
|
|
|
|
|
}
|
2016-08-17 23:47:54 +02:00
|
|
|
|
|
2016-08-16 00:59:31 +02:00
|
|
|
|
const nullLog: Log = {
|
|
|
|
|
isEnabled: () => false,
|
|
|
|
|
writeLine: () => {}
|
2016-08-17 23:47:54 +02:00
|
|
|
|
};
|
2016-08-16 00:59:31 +02:00
|
|
|
|
|
2016-08-26 01:25:34 +02:00
|
|
|
|
function typingToFileName(cachePath: string, packageName: string, installTypingHost: InstallTypingHost): string {
|
|
|
|
|
const result = resolveModuleName(packageName, combinePaths(cachePath, "index.d.ts"), { moduleResolution: ModuleResolutionKind.NodeJs }, installTypingHost);
|
2016-08-27 01:37:31 +02:00
|
|
|
|
return result.resolvedModule && result.resolvedModule.resolvedFileName;
|
2016-08-15 20:48:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
2016-10-01 20:56:51 +02:00
|
|
|
|
export enum PackageNameValidationResult {
|
|
|
|
|
Ok,
|
|
|
|
|
ScopedPackagesNotSupported,
|
|
|
|
|
NameTooLong,
|
|
|
|
|
NameStartsWithDot,
|
|
|
|
|
NameStartsWithUnderscore,
|
|
|
|
|
NameContainsNonURISafeCharacters
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const MaxPackageNameLength = 214;
|
|
|
|
|
/**
|
|
|
|
|
* Validates package name using rules defined at https://docs.npmjs.com/files/package.json
|
|
|
|
|
*/
|
|
|
|
|
export function validatePackageName(packageName: string): PackageNameValidationResult {
|
|
|
|
|
Debug.assert(!!packageName, "Package name is not specified");
|
|
|
|
|
if (packageName.length > MaxPackageNameLength) {
|
|
|
|
|
return PackageNameValidationResult.NameTooLong;
|
|
|
|
|
}
|
|
|
|
|
if (packageName.charCodeAt(0) === CharacterCodes.dot) {
|
|
|
|
|
return PackageNameValidationResult.NameStartsWithDot;
|
|
|
|
|
}
|
|
|
|
|
if (packageName.charCodeAt(0) === CharacterCodes._) {
|
|
|
|
|
return PackageNameValidationResult.NameStartsWithUnderscore;
|
|
|
|
|
}
|
|
|
|
|
// check if name is scope package like: starts with @ and has one '/' in the middle
|
|
|
|
|
// scoped packages are not currently supported
|
|
|
|
|
// TODO: when support will be added we'll need to split and check both scope and package name
|
|
|
|
|
if (/^@[^/]+\/[^/]+$/.test(packageName)) {
|
|
|
|
|
return PackageNameValidationResult.ScopedPackagesNotSupported;
|
|
|
|
|
}
|
|
|
|
|
if (encodeURIComponent(packageName) !== packageName) {
|
|
|
|
|
return PackageNameValidationResult.NameContainsNonURISafeCharacters;
|
|
|
|
|
}
|
|
|
|
|
return PackageNameValidationResult.Ok;
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-20 23:14:51 +02:00
|
|
|
|
export const NpmViewRequest: "npm view" = "npm view";
|
|
|
|
|
export const NpmInstallRequest: "npm install" = "npm install";
|
|
|
|
|
|
|
|
|
|
export type RequestKind = typeof NpmViewRequest | typeof NpmInstallRequest;
|
|
|
|
|
|
|
|
|
|
export type RequestCompletedAction = (err: Error, stdout: string, stderr: string) => void;
|
|
|
|
|
type PendingRequest = {
|
|
|
|
|
requestKind: RequestKind;
|
|
|
|
|
requestId: number;
|
|
|
|
|
command: string;
|
|
|
|
|
cwd: string;
|
|
|
|
|
onRequestCompleted: RequestCompletedAction
|
|
|
|
|
};
|
|
|
|
|
|
2016-08-12 20:04:43 +02:00
|
|
|
|
export abstract class TypingsInstaller {
|
2016-09-20 23:14:51 +02:00
|
|
|
|
private readonly packageNameToTypingLocation: Map<string> = createMap<string>();
|
|
|
|
|
private readonly missingTypingsSet: Map<true> = createMap<true>();
|
|
|
|
|
private readonly knownCachesSet: Map<true> = createMap<true>();
|
|
|
|
|
private readonly projectWatchers: Map<FileWatcher[]> = createMap<FileWatcher[]>();
|
|
|
|
|
readonly pendingRunRequests: PendingRequest[] = [];
|
|
|
|
|
|
2016-09-19 22:56:30 +02:00
|
|
|
|
private installRunCount = 1;
|
2016-09-20 23:14:51 +02:00
|
|
|
|
private inFlightRequestCount = 0;
|
2016-08-16 23:21:09 +02:00
|
|
|
|
|
2016-08-15 20:48:28 +02:00
|
|
|
|
abstract readonly installTypingHost: InstallTypingHost;
|
|
|
|
|
|
2016-09-20 23:14:51 +02:00
|
|
|
|
constructor(
|
|
|
|
|
readonly globalCachePath: string,
|
|
|
|
|
readonly npmPath: string,
|
|
|
|
|
readonly safeListPath: Path,
|
|
|
|
|
readonly throttleLimit: number,
|
|
|
|
|
protected readonly log = nullLog) {
|
2016-08-16 00:59:31 +02:00
|
|
|
|
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() {
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-17 22:01:48 +02:00
|
|
|
|
private closeWatchers(projectName: string): void {
|
2016-08-16 23:21:09 +02:00
|
|
|
|
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}'`);
|
|
|
|
|
}
|
2016-08-17 22:01:48 +02:00
|
|
|
|
return;
|
2016-08-16 23:21:09 +02:00
|
|
|
|
}
|
|
|
|
|
for (const w of watchers) {
|
|
|
|
|
w.close();
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-17 23:47:54 +02:00
|
|
|
|
delete this.projectWatchers[projectName];
|
2016-08-16 23:21:09 +02:00
|
|
|
|
|
|
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
|
this.log.writeLine(`Closing file watchers for project '${projectName}' - done.`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
install(req: DiscoverTypings) {
|
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-17 23:47:54 +02:00
|
|
|
|
|
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
|
2016-08-17 22:01:48 +02:00
|
|
|
|
if (discoverTypingsResult.newTypingNames.length) {
|
|
|
|
|
this.installTypings(req, req.cachePath || this.globalCachePath, discoverTypingsResult.cachedTypingPaths, discoverTypingsResult.newTypingNames);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
|
this.log.writeLine(`No new typings were requested as a result of typings discovery`);
|
|
|
|
|
}
|
|
|
|
|
}
|
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()) {
|
2016-08-17 23:47:54 +02:00
|
|
|
|
this.log.writeLine(`Cache location was already processed...`);
|
2016-08-16 00:59:31 +02:00
|
|
|
|
}
|
2016-08-15 20:48:28 +02:00
|
|
|
|
return;
|
|
|
|
|
}
|
2016-08-26 01:25:34 +02:00
|
|
|
|
const packageJson = combinePaths(cacheLocation, "package.json");
|
2016-08-16 00:59:31 +02:00
|
|
|
|
if (this.log.isEnabled()) {
|
2016-08-26 01:25:34 +02:00
|
|
|
|
this.log.writeLine(`Trying to find '${packageJson}'...`);
|
2016-08-16 00:59:31 +02:00
|
|
|
|
}
|
2016-08-26 01:25:34 +02:00
|
|
|
|
if (this.installTypingHost.fileExists(packageJson)) {
|
|
|
|
|
const npmConfig = <NpmConfig>JSON.parse(this.installTypingHost.readFile(packageJson));
|
2016-08-16 00:59:31 +02:00
|
|
|
|
if (this.log.isEnabled()) {
|
2016-09-22 23:34:08 +02:00
|
|
|
|
this.log.writeLine(`Loaded content of '${packageJson}': ${JSON.stringify(npmConfig)}`);
|
2016-08-16 00:59:31 +02:00
|
|
|
|
}
|
2016-08-26 01:25:34 +02:00
|
|
|
|
if (npmConfig.devDependencies) {
|
|
|
|
|
for (const key in npmConfig.devDependencies) {
|
|
|
|
|
// key is @types/<package name>
|
2016-08-27 01:37:31 +02:00
|
|
|
|
const packageName = getBaseFileName(key);
|
2016-08-15 20:48:28 +02:00
|
|
|
|
if (!packageName) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2016-09-02 03:28:37 +02:00
|
|
|
|
const typingFile = typingToFileName(cacheLocation, packageName, this.installTypingHost);
|
2016-08-26 01:25:34 +02:00
|
|
|
|
if (!typingFile) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2016-08-15 20:48:28 +02:00
|
|
|
|
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-10-01 20:56:51 +02:00
|
|
|
|
private filterTypings(typingsToInstall: string[]) {
|
|
|
|
|
if (typingsToInstall.length === 0) {
|
|
|
|
|
return typingsToInstall;
|
|
|
|
|
}
|
|
|
|
|
const result: string[] = [];
|
|
|
|
|
for (const typing of typingsToInstall) {
|
|
|
|
|
if (this.missingTypingsSet[typing]) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const validationResult = validatePackageName(typing);
|
|
|
|
|
if (validationResult === PackageNameValidationResult.Ok) {
|
|
|
|
|
result.push(typing);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
// add typing name to missing set so we won't process it again
|
|
|
|
|
this.missingTypingsSet[typing] = true;
|
|
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
|
switch (validationResult) {
|
|
|
|
|
case PackageNameValidationResult.NameTooLong:
|
|
|
|
|
this.log.writeLine(`Package name '${typing}' should be less than ${MaxPackageNameLength} characters`);
|
|
|
|
|
break;
|
|
|
|
|
case PackageNameValidationResult.NameStartsWithDot:
|
|
|
|
|
this.log.writeLine(`Package name '${typing}' cannot start with '.'`);
|
|
|
|
|
break;
|
|
|
|
|
case PackageNameValidationResult.NameStartsWithUnderscore:
|
|
|
|
|
this.log.writeLine(`Package name '${typing}' cannot start with '_'`);
|
|
|
|
|
break;
|
|
|
|
|
case PackageNameValidationResult.ScopedPackagesNotSupported:
|
|
|
|
|
this.log.writeLine(`Package '${typing}' is scoped and currently is not supported`);
|
|
|
|
|
break;
|
|
|
|
|
case PackageNameValidationResult.NameContainsNonURISafeCharacters:
|
|
|
|
|
this.log.writeLine(`Package name '${typing}' contains non URI safe characters`);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
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-10-01 20:56:51 +02:00
|
|
|
|
typingsToInstall = this.filterTypings(typingsToInstall);
|
2016-08-12 21:14:25 +02:00
|
|
|
|
if (typingsToInstall.length === 0) {
|
2016-08-16 00:59:31 +02:00
|
|
|
|
if (this.log.isEnabled()) {
|
2016-10-01 20:56:51 +02:00
|
|
|
|
this.log.writeLine(`All typings are known to be missing or invalid - no need to go any further`);
|
2016-08-16 00:59:31 +02:00
|
|
|
|
}
|
2016-08-12 21:14:25 +02:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-26 01:25:34 +02:00
|
|
|
|
const npmConfigPath = combinePaths(cachePath, "package.json");
|
2016-08-16 00:59:31 +02:00
|
|
|
|
if (this.log.isEnabled()) {
|
2016-08-26 01:25:34 +02:00
|
|
|
|
this.log.writeLine(`Npm config file: ${npmConfigPath}`);
|
2016-08-16 00:59:31 +02:00
|
|
|
|
}
|
2016-08-26 01:25:34 +02:00
|
|
|
|
if (!this.installTypingHost.fileExists(npmConfigPath)) {
|
2016-08-16 00:59:31 +02:00
|
|
|
|
if (this.log.isEnabled()) {
|
2016-08-26 01:25:34 +02:00
|
|
|
|
this.log.writeLine(`Npm config file: '${npmConfigPath}' is missing, creating new one...`);
|
2016-08-16 00:59:31 +02:00
|
|
|
|
}
|
|
|
|
|
this.ensureDirectoryExists(cachePath, this.installTypingHost);
|
2016-08-26 01:25:34 +02:00
|
|
|
|
this.installTypingHost.writeFile(npmConfigPath, "{}");
|
2016-08-12 20:04:43 +02:00
|
|
|
|
}
|
2016-08-12 21:14:25 +02:00
|
|
|
|
|
2016-08-26 01:25:34 +02:00
|
|
|
|
this.runInstall(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) {
|
2016-08-27 01:37:31 +02:00
|
|
|
|
const packageName = getBaseFileName(t);
|
2016-08-15 20:48:28 +02:00
|
|
|
|
if (!packageName) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
installedPackages[packageName] = true;
|
2016-09-02 03:28:37 +02:00
|
|
|
|
const typingFile = typingToFileName(cachePath, packageName, this.installTypingHost);
|
2016-08-26 01:25:34 +02:00
|
|
|
|
if (!typingFile) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2016-09-01 06:14:24 +02:00
|
|
|
|
if (!this.packageNameToTypingLocation[packageName]) {
|
|
|
|
|
this.packageNameToTypingLocation[packageName] = typingFile;
|
|
|
|
|
}
|
2016-08-26 01:25:34 +02:00
|
|
|
|
installedTypingFiles.push(typingFile);
|
2016-08-16 00:59:31 +02:00
|
|
|
|
}
|
|
|
|
|
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-09-19 22:56:30 +02:00
|
|
|
|
private runInstall(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void): void {
|
2016-09-20 23:14:51 +02:00
|
|
|
|
const requestId = this.installRunCount;
|
|
|
|
|
|
2016-09-19 22:56:30 +02:00
|
|
|
|
this.installRunCount++;
|
|
|
|
|
let execInstallCmdCount = 0;
|
|
|
|
|
const filteredTypings: string[] = [];
|
|
|
|
|
for (const typing of typingsToInstall) {
|
|
|
|
|
execNpmViewTyping(this, typing);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function execNpmViewTyping(self: TypingsInstaller, typing: string) {
|
|
|
|
|
const command = `${self.npmPath} view @types/${typing} --silent name`;
|
2016-09-20 23:14:51 +02:00
|
|
|
|
self.execAsync(NpmViewRequest, requestId, command, cachePath, (err, stdout, stderr) => {
|
2016-09-19 22:56:30 +02:00
|
|
|
|
if (stdout) {
|
|
|
|
|
filteredTypings.push(typing);
|
|
|
|
|
}
|
|
|
|
|
execInstallCmdCount++;
|
|
|
|
|
if (execInstallCmdCount === typingsToInstall.length) {
|
|
|
|
|
installFilteredTypings(self, filteredTypings);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function installFilteredTypings(self: TypingsInstaller, filteredTypings: string[]) {
|
|
|
|
|
if (filteredTypings.length === 0) {
|
2016-09-20 23:14:51 +02:00
|
|
|
|
postInstallAction([]);
|
2016-09-19 22:56:30 +02:00
|
|
|
|
return;
|
|
|
|
|
}
|
2016-09-20 23:14:51 +02:00
|
|
|
|
const scopedTypings = filteredTypings.map(t => "@types/" + t);
|
|
|
|
|
const command = `${self.npmPath} install ${scopedTypings.join(" ")} --save-dev`;
|
|
|
|
|
self.execAsync(NpmInstallRequest, requestId, command, cachePath, (err, stdout, stderr) => {
|
|
|
|
|
postInstallAction(stdout ? scopedTypings : []);
|
2016-09-19 22:56:30 +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;
|
|
|
|
|
}
|
2016-08-17 22:01:48 +02:00
|
|
|
|
// shut down existing watchers
|
|
|
|
|
this.closeWatchers(projectName);
|
|
|
|
|
|
|
|
|
|
// handler should be invoked once for the entire set of files since it will trigger full rediscovery of typings
|
|
|
|
|
let isInvoked = false;
|
2016-08-16 23:21:09 +02:00
|
|
|
|
const watchers: FileWatcher[] = [];
|
|
|
|
|
for (const file of files) {
|
|
|
|
|
const w = this.installTypingHost.watchFile(file, f => {
|
|
|
|
|
if (this.log.isEnabled()) {
|
2016-08-17 22:01:48 +02:00
|
|
|
|
this.log.writeLine(`Got FS notification for ${f}, handler is already invoked '${isInvoked}'`);
|
2016-08-16 23:21:09 +02:00
|
|
|
|
}
|
2016-08-17 22:01:48 +02:00
|
|
|
|
this.sendResponse({ projectName: projectName, kind: "invalidate" });
|
|
|
|
|
isInvoked = true;
|
2016-08-16 23:21:09 +02:00
|
|
|
|
});
|
|
|
|
|
watchers.push(w);
|
|
|
|
|
}
|
2016-08-17 22:01:48 +02:00
|
|
|
|
this.projectWatchers[projectName] = watchers;
|
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
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-20 23:14:51 +02:00
|
|
|
|
private execAsync(requestKind: RequestKind, requestId: number, command: string, cwd: string, onRequestCompleted: RequestCompletedAction): void {
|
|
|
|
|
this.pendingRunRequests.unshift({ requestKind, requestId, command, cwd, onRequestCompleted });
|
|
|
|
|
this.executeWithThrottling();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private executeWithThrottling() {
|
|
|
|
|
while (this.inFlightRequestCount < this.throttleLimit && this.pendingRunRequests.length) {
|
|
|
|
|
this.inFlightRequestCount++;
|
|
|
|
|
const request = this.pendingRunRequests.pop();
|
|
|
|
|
this.runCommand(request.requestKind, request.requestId, request.command, request.cwd, (err, stdout, stderr) => {
|
|
|
|
|
this.inFlightRequestCount--;
|
|
|
|
|
request.onRequestCompleted(err, stdout, stderr);
|
|
|
|
|
this.executeWithThrottling();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected abstract runCommand(requestKind: RequestKind, requestId: number, command: string, cwd: string, onRequestCompleted: RequestCompletedAction): void;
|
2016-08-16 23:21:09 +02:00
|
|
|
|
protected abstract sendResponse(response: SetTypings | InvalidateCachedTypings): void;
|
2016-08-12 20:04:43 +02:00
|
|
|
|
}
|
|
|
|
|
}
|