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
|
|
|
}
|
|
|
|
|
2018-01-11 18:13:33 +01:00
|
|
|
interface NpmLock {
|
|
|
|
dependencies: { [packageName: string]: { version: string } };
|
|
|
|
}
|
|
|
|
|
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,
|
2016-10-19 23:15:14 +02:00
|
|
|
writeLine: noop
|
2016-08-17 23:47:54 +02:00
|
|
|
};
|
2016-08-16 00:59:31 +02:00
|
|
|
|
2018-05-22 23:46:57 +02:00
|
|
|
function typingToFileName(cachePath: string, packageName: string, installTypingHost: InstallTypingHost, log: Log): string | undefined {
|
2016-11-15 21:53:46 +01:00
|
|
|
try {
|
|
|
|
const result = resolveModuleName(packageName, combinePaths(cachePath, "index.d.ts"), { moduleResolution: ModuleResolutionKind.NodeJs }, installTypingHost);
|
|
|
|
return result.resolvedModule && result.resolvedModule.resolvedFileName;
|
|
|
|
}
|
|
|
|
catch (e) {
|
|
|
|
if (log.isEnabled()) {
|
2021-05-18 15:20:57 +02:00
|
|
|
log.writeLine(`Failed to resolve ${packageName} in folder '${cachePath}': ${(e as Error).message}`);
|
2016-11-15 21:53:46 +01:00
|
|
|
}
|
|
|
|
return undefined;
|
|
|
|
}
|
2016-08-15 20:48:28 +02:00
|
|
|
}
|
|
|
|
|
2018-04-12 20:43:18 +02:00
|
|
|
/*@internal*/
|
2019-02-26 23:01:42 +01:00
|
|
|
export function installNpmPackages(npmPath: string, tsVersion: string, packageNames: string[], install: (command: string) => boolean) {
|
2018-04-12 20:43:18 +02:00
|
|
|
let hasError = false;
|
|
|
|
for (let remaining = packageNames.length; remaining > 0;) {
|
|
|
|
const result = getNpmCommandForInstallation(npmPath, tsVersion, packageNames, remaining);
|
|
|
|
remaining = result.remaining;
|
2019-02-26 23:01:42 +01:00
|
|
|
hasError = install(result.command) || hasError;
|
2018-04-12 20:43:18 +02:00
|
|
|
}
|
|
|
|
return hasError;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*@internal*/
|
|
|
|
export function getNpmCommandForInstallation(npmPath: string, tsVersion: string, packageNames: string[], remaining: number) {
|
|
|
|
const sliceStart = packageNames.length - remaining;
|
2019-02-26 23:01:42 +01:00
|
|
|
let command: string, toSlice = remaining;
|
2018-04-12 20:43:18 +02:00
|
|
|
while (true) {
|
2019-02-26 23:01:42 +01:00
|
|
|
command = `${npmPath} install --ignore-scripts ${(toSlice === packageNames.length ? packageNames : packageNames.slice(sliceStart, sliceStart + toSlice)).join(" ")} --save-dev --user-agent="typesInstaller/${tsVersion}"`;
|
|
|
|
if (command.length < 8000) {
|
2018-04-12 20:43:18 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
toSlice = toSlice - Math.floor(toSlice / 2);
|
|
|
|
}
|
|
|
|
return { command, remaining: remaining - toSlice };
|
|
|
|
}
|
2016-10-01 20:56:51 +02:00
|
|
|
|
2016-10-13 19:19:18 +02:00
|
|
|
export type RequestCompletedAction = (success: boolean) => void;
|
2017-09-07 18:14:59 +02:00
|
|
|
interface PendingRequest {
|
2016-09-20 23:14:51 +02:00
|
|
|
requestId: number;
|
2017-10-18 00:04:09 +02:00
|
|
|
packageNames: string[];
|
2016-09-20 23:14:51 +02:00
|
|
|
cwd: string;
|
2016-11-10 23:28:34 +01:00
|
|
|
onRequestCompleted: RequestCompletedAction;
|
2017-09-07 18:14:59 +02:00
|
|
|
}
|
2016-09-20 23:14:51 +02:00
|
|
|
|
2018-07-19 22:28:55 +02:00
|
|
|
function endsWith(str: string, suffix: string, caseSensitive: boolean): boolean {
|
|
|
|
const expectedPos = str.length - suffix.length;
|
|
|
|
return expectedPos >= 0 &&
|
|
|
|
(str.indexOf(suffix, expectedPos) === expectedPos ||
|
|
|
|
(!caseSensitive && compareStringsCaseInsensitive(str.substr(expectedPos), suffix) === Comparison.EqualTo));
|
2018-04-17 21:18:49 +02:00
|
|
|
}
|
|
|
|
|
2018-07-19 22:28:55 +02:00
|
|
|
function isPackageOrBowerJson(fileName: string, caseSensitive: boolean) {
|
|
|
|
return endsWith(fileName, "/package.json", caseSensitive) || endsWith(fileName, "/bower.json", caseSensitive);
|
|
|
|
}
|
|
|
|
|
|
|
|
function sameFiles(a: string, b: string, caseSensitive: boolean) {
|
|
|
|
return a === b || (!caseSensitive && compareStringsCaseInsensitive(a, b) === Comparison.EqualTo);
|
|
|
|
}
|
|
|
|
|
|
|
|
const enum ProjectWatcherType {
|
|
|
|
FileWatcher = "FileWatcher",
|
|
|
|
DirectoryWatcher = "DirectoryWatcher"
|
2018-04-17 21:18:49 +02:00
|
|
|
}
|
|
|
|
|
2020-07-02 02:00:26 +02:00
|
|
|
type ProjectWatchers = ESMap<string, FileWatcher> & { isInvoked?: boolean; };
|
2018-04-14 00:15:09 +02:00
|
|
|
|
2020-11-04 22:30:06 +01:00
|
|
|
function getDetailWatchInfo(projectName: string, watchers: ProjectWatchers) {
|
|
|
|
return `Project: ${projectName} watcher already invoked: ${watchers.isInvoked}`;
|
|
|
|
}
|
|
|
|
|
2016-08-12 20:04:43 +02:00
|
|
|
export abstract class TypingsInstaller {
|
2020-07-07 22:53:46 +02:00
|
|
|
private readonly packageNameToTypingLocation = new Map<string, JsTyping.CachedTyping>();
|
2020-06-26 19:12:47 +02:00
|
|
|
private readonly missingTypingsSet = new Set<string>();
|
|
|
|
private readonly knownCachesSet = new Set<string>();
|
2020-06-26 01:03:25 +02:00
|
|
|
private readonly projectWatchers = new Map<string, ProjectWatchers>();
|
2017-07-27 19:54:47 +02:00
|
|
|
private safeList: JsTyping.SafeList | undefined;
|
2016-09-20 23:14:51 +02:00
|
|
|
readonly pendingRunRequests: PendingRequest[] = [];
|
2018-04-17 21:18:49 +02:00
|
|
|
private readonly toCanonicalFileName: GetCanonicalFileName;
|
2018-07-19 22:28:55 +02:00
|
|
|
private readonly globalCachePackageJsonPath: string;
|
2016-09-20 23:14:51 +02:00
|
|
|
|
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
|
|
|
|
2020-07-02 02:00:26 +02:00
|
|
|
abstract readonly typesRegistry: ESMap<string, MapLike<string>>;
|
2020-11-04 22:30:06 +01:00
|
|
|
/*@internal*/
|
|
|
|
private readonly watchFactory: WatchFactory<string, ProjectWatchers>;
|
2016-08-15 20:48:28 +02:00
|
|
|
|
2016-09-20 23:14:51 +02:00
|
|
|
constructor(
|
2017-07-14 23:26:13 +02:00
|
|
|
protected readonly installTypingHost: InstallTypingHost,
|
|
|
|
private readonly globalCachePath: string,
|
|
|
|
private readonly safeListPath: Path,
|
2017-07-28 01:07:50 +02:00
|
|
|
private readonly typesMapLocation: Path,
|
2017-07-14 23:26:13 +02:00
|
|
|
private readonly throttleLimit: number,
|
2016-09-20 23:14:51 +02:00
|
|
|
protected readonly log = nullLog) {
|
2018-04-17 21:18:49 +02:00
|
|
|
this.toCanonicalFileName = createGetCanonicalFileName(installTypingHost.useCaseSensitiveFileNames);
|
2018-07-19 22:28:55 +02:00
|
|
|
this.globalCachePackageJsonPath = combinePaths(globalCachePath, "package.json");
|
2020-11-04 22:30:06 +01:00
|
|
|
const isLoggingEnabled = this.log.isEnabled();
|
|
|
|
if (isLoggingEnabled) {
|
2017-07-28 01:07:50 +02:00
|
|
|
this.log.writeLine(`Global cache location '${globalCachePath}', safe file path '${safeListPath}', types map path ${typesMapLocation}`);
|
2016-08-16 00:59:31 +02:00
|
|
|
}
|
2020-11-04 22:30:06 +01:00
|
|
|
this.watchFactory = getWatchFactory(this.installTypingHost as WatchFactoryHost, isLoggingEnabled ? WatchLogLevel.Verbose : WatchLogLevel.None, s => this.log.writeLine(s), getDetailWatchInfo);
|
2018-01-05 01:44:01 +01: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}'`);
|
|
|
|
}
|
2016-12-05 23:13:32 +01:00
|
|
|
const watchers = this.projectWatchers.get(projectName);
|
2016-08-16 23:21:09 +02:00
|
|
|
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
|
|
|
}
|
2018-03-15 18:44:09 +01:00
|
|
|
clearMap(watchers, closeFileWatcher);
|
2016-12-05 23:13:32 +01:00
|
|
|
this.projectWatchers.delete(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-09-26 20:33:25 +02:00
|
|
|
// load existing typing information from the cache
|
2016-08-15 20:48:28 +02:00
|
|
|
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...`);
|
|
|
|
}
|
2018-01-11 20:11:26 +01:00
|
|
|
this.processCacheLocation(req.cachePath);
|
2016-08-15 20:48:28 +02:00
|
|
|
}
|
|
|
|
|
2017-07-27 19:54:47 +02:00
|
|
|
if (this.safeList === undefined) {
|
2017-07-28 01:07:50 +02:00
|
|
|
this.initializeSafeList();
|
2017-07-27 19:54:47 +02:00
|
|
|
}
|
2016-08-12 20:04:43 +02:00
|
|
|
const discoverTypingsResult = JsTyping.discoverTypings(
|
2016-08-15 20:48:28 +02:00
|
|
|
this.installTypingHost,
|
2017-08-23 22:01:14 +02:00
|
|
|
this.log.isEnabled() ? (s => this.log.writeLine(s)) : undefined,
|
2016-08-12 20:04:43 +02:00
|
|
|
req.fileNames,
|
|
|
|
req.projectRootPath,
|
2018-05-22 23:46:57 +02:00
|
|
|
this.safeList!,
|
2016-08-15 20:48:28 +02:00
|
|
|
this.packageNameToTypingLocation,
|
2016-11-19 02:46:06 +01:00
|
|
|
req.typeAcquisition,
|
2018-01-11 20:11:26 +01:00
|
|
|
req.unresolvedImports,
|
|
|
|
this.typesRegistry);
|
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
|
|
|
|
2016-08-12 21:14:25 +02:00
|
|
|
// start watching files
|
2019-12-11 22:26:44 +01:00
|
|
|
this.watchFiles(req.projectName, discoverTypingsResult.filesToWatch, req.projectRootPath, req.watchOptions);
|
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) {
|
2018-01-11 20:11:26 +01:00
|
|
|
this.installTypings(req, req.cachePath || this.globalCachePath, discoverTypingsResult.cachedTypingPaths, discoverTypingsResult.newTypingNames);
|
2016-08-17 22:01:48 +02:00
|
|
|
}
|
|
|
|
else {
|
2017-11-16 21:37:40 +01:00
|
|
|
this.sendResponse(this.createSetTypings(req, discoverTypingsResult.cachedTypingPaths));
|
2016-08-17 22:01:48 +02:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2017-07-28 01:07:50 +02:00
|
|
|
private initializeSafeList() {
|
|
|
|
// Prefer the safe list from the types map if it exists
|
|
|
|
if (this.typesMapLocation) {
|
|
|
|
const safeListFromMap = JsTyping.loadTypesMap(this.installTypingHost, this.typesMapLocation);
|
|
|
|
if (safeListFromMap) {
|
|
|
|
this.log.writeLine(`Loaded safelist from types map file '${this.typesMapLocation}'`);
|
|
|
|
this.safeList = safeListFromMap;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.log.writeLine(`Failed to load safelist from types map file '${this.typesMapLocation}'`);
|
|
|
|
}
|
|
|
|
this.safeList = JsTyping.loadSafeList(this.installTypingHost, this.safeListPath);
|
|
|
|
}
|
|
|
|
|
2018-01-11 20:11:26 +01:00
|
|
|
private processCacheLocation(cacheLocation: string) {
|
2016-08-16 00:59:31 +02:00
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`Processing cache location '${cacheLocation}'`);
|
|
|
|
}
|
2018-01-11 20:11:26 +01:00
|
|
|
if (this.knownCachesSet.has(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");
|
2018-01-11 18:13:33 +01:00
|
|
|
const packageLockJson = combinePaths(cacheLocation, "package-lock.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
|
|
|
}
|
2018-01-11 18:13:33 +01:00
|
|
|
if (this.installTypingHost.fileExists(packageJson) && this.installTypingHost.fileExists(packageLockJson)) {
|
2021-05-18 15:20:57 +02:00
|
|
|
const npmConfig = JSON.parse(this.installTypingHost.readFile(packageJson)!) as NpmConfig; // TODO: GH#18217
|
|
|
|
const npmLock = JSON.parse(this.installTypingHost.readFile(packageLockJson)!) as NpmLock; // TODO: GH#18217
|
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)}`);
|
2018-01-11 18:13:33 +01:00
|
|
|
this.log.writeLine(`Loaded content of '${packageLockJson}'`);
|
2016-08-16 00:59:31 +02:00
|
|
|
}
|
2018-01-11 18:13:33 +01:00
|
|
|
if (npmConfig.devDependencies && npmLock.dependencies) {
|
2016-08-26 01:25:34 +02:00
|
|
|
for (const key in npmConfig.devDependencies) {
|
2018-02-09 19:36:49 +01:00
|
|
|
if (!hasProperty(npmLock.dependencies, key)) {
|
|
|
|
// if package in package.json but not package-lock.json, skip adding to cache so it is reinstalled on next use
|
|
|
|
continue;
|
|
|
|
}
|
2016-08-26 01:25:34 +02:00
|
|
|
// 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-11-15 21:53:46 +01:00
|
|
|
const typingFile = typingToFileName(cacheLocation, packageName, this.installTypingHost, this.log);
|
2016-08-26 01:25:34 +02:00
|
|
|
if (!typingFile) {
|
2020-06-26 19:12:47 +02:00
|
|
|
this.missingTypingsSet.add(packageName);
|
2016-08-26 01:25:34 +02:00
|
|
|
continue;
|
|
|
|
}
|
2016-12-05 23:13:32 +01:00
|
|
|
const existingTypingFile = this.packageNameToTypingLocation.get(packageName);
|
2016-08-15 20:48:28 +02:00
|
|
|
if (existingTypingFile) {
|
2017-12-29 23:21:55 +01:00
|
|
|
if (existingTypingFile.typingLocation === typingFile) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|
2018-01-11 18:13:33 +01:00
|
|
|
const info = getProperty(npmLock.dependencies, key);
|
|
|
|
const version = info && info.version;
|
2018-08-22 20:22:09 +02:00
|
|
|
if (!version) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const newTyping: JsTyping.CachedTyping = { typingLocation: typingFile, version: new Version(version) };
|
2017-11-21 01:43:02 +01:00
|
|
|
this.packageNameToTypingLocation.set(packageName, newTyping);
|
2016-08-15 20:48:28 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-08-16 00:59:31 +02:00
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`Finished processing cache location '${cacheLocation}'`);
|
|
|
|
}
|
2020-06-26 19:12:47 +02:00
|
|
|
this.knownCachesSet.add(cacheLocation);
|
2016-08-15 20:48:28 +02:00
|
|
|
}
|
|
|
|
|
2019-08-08 20:30:18 +02:00
|
|
|
private filterTypings(typingsToInstall: readonly string[]): readonly string[] {
|
2019-07-16 19:14:06 +02:00
|
|
|
return mapDefined(typingsToInstall, typing => {
|
|
|
|
const typingKey = mangleScopedPackageName(typing);
|
2020-06-26 19:12:47 +02:00
|
|
|
if (this.missingTypingsSet.has(typingKey)) {
|
2019-07-16 19:14:06 +02:00
|
|
|
if (this.log.isEnabled()) this.log.writeLine(`'${typing}':: '${typingKey}' is in missingTypingsSet - skipping...`);
|
|
|
|
return undefined;
|
2016-10-01 20:56:51 +02:00
|
|
|
}
|
2017-12-04 22:36:01 +01:00
|
|
|
const validationResult = JsTyping.validatePackageName(typing);
|
2019-07-16 19:14:06 +02:00
|
|
|
if (validationResult !== JsTyping.NameValidationResult.Ok) {
|
2016-10-01 20:56:51 +02:00
|
|
|
// add typing name to missing set so we won't process it again
|
2020-06-26 19:12:47 +02:00
|
|
|
this.missingTypingsSet.add(typingKey);
|
2017-12-04 22:36:01 +01:00
|
|
|
if (this.log.isEnabled()) this.log.writeLine(JsTyping.renderPackageNameValidationFailure(validationResult, typing));
|
2019-07-16 19:14:06 +02:00
|
|
|
return undefined;
|
2016-10-01 20:56:51 +02:00
|
|
|
}
|
2019-07-16 19:14:06 +02:00
|
|
|
if (!this.typesRegistry.has(typingKey)) {
|
|
|
|
if (this.log.isEnabled()) this.log.writeLine(`'${typing}':: Entry for package '${typingKey}' does not exist in local types registry - skipping...`);
|
|
|
|
return undefined;
|
2017-12-04 22:36:01 +01:00
|
|
|
}
|
2019-07-16 19:14:06 +02:00
|
|
|
if (this.packageNameToTypingLocation.get(typingKey) && JsTyping.isTypingUpToDate(this.packageNameToTypingLocation.get(typingKey)!, this.typesRegistry.get(typingKey)!)) {
|
|
|
|
if (this.log.isEnabled()) this.log.writeLine(`'${typing}':: '${typingKey}' already has an up-to-date typing - skipping...`);
|
|
|
|
return undefined;
|
2018-01-11 18:13:33 +01:00
|
|
|
}
|
2019-07-16 19:14:06 +02:00
|
|
|
return typingKey;
|
2017-12-04 22:36:01 +01:00
|
|
|
});
|
2016-10-01 20:56:51 +02:00
|
|
|
}
|
|
|
|
|
2016-11-07 22:36:08 +01:00
|
|
|
protected ensurePackageDirectoryExists(directory: string) {
|
|
|
|
const npmConfigPath = combinePaths(directory, "package.json");
|
2016-08-16 00:59:31 +02:00
|
|
|
if (this.log.isEnabled()) {
|
2016-11-07 22:36:08 +01:00
|
|
|
this.log.writeLine(`Npm config file: ${npmConfigPath}`);
|
2016-08-16 00:59:31 +02:00
|
|
|
}
|
2016-11-07 22:36:08 +01:00
|
|
|
if (!this.installTypingHost.fileExists(npmConfigPath)) {
|
2016-08-16 00:59:31 +02:00
|
|
|
if (this.log.isEnabled()) {
|
2016-11-07 22:36:08 +01:00
|
|
|
this.log.writeLine(`Npm config file: '${npmConfigPath}' is missing, creating new one...`);
|
2016-08-16 00:59:31 +02:00
|
|
|
}
|
2016-11-07 22:36:08 +01:00
|
|
|
this.ensureDirectoryExists(directory, this.installTypingHost);
|
2017-11-06 18:45:52 +01:00
|
|
|
this.installTypingHost.writeFile(npmConfigPath, '{ "private": true }');
|
2016-08-12 21:14:25 +02:00
|
|
|
}
|
2016-11-07 22:36:08 +01:00
|
|
|
}
|
2016-08-12 21:14:25 +02:00
|
|
|
|
2018-01-11 20:11:26 +01:00
|
|
|
private installTypings(req: DiscoverTypings, cachePath: string, currentlyCachedTypings: string[], typingsToInstall: string[]) {
|
2016-08-16 00:59:31 +02:00
|
|
|
if (this.log.isEnabled()) {
|
2016-11-07 22:36:08 +01:00
|
|
|
this.log.writeLine(`Installing typings ${JSON.stringify(typingsToInstall)}`);
|
2016-08-16 00:59:31 +02:00
|
|
|
}
|
2016-11-11 01:52:36 +01:00
|
|
|
const filteredTypings = this.filterTypings(typingsToInstall);
|
2017-01-30 16:49:17 +01:00
|
|
|
if (filteredTypings.length === 0) {
|
2016-08-16 00:59:31 +02:00
|
|
|
if (this.log.isEnabled()) {
|
2017-11-20 23:43:25 +01:00
|
|
|
this.log.writeLine(`All typings are known to be missing or invalid - no need to install more typings`);
|
2016-08-16 00:59:31 +02:00
|
|
|
}
|
2017-11-20 23:43:25 +01:00
|
|
|
this.sendResponse(this.createSetTypings(req, currentlyCachedTypings));
|
2016-11-07 22:36:08 +01:00
|
|
|
return;
|
2016-08-12 20:04:43 +02:00
|
|
|
}
|
2016-08-12 21:14:25 +02:00
|
|
|
|
2016-11-07 22:36:08 +01:00
|
|
|
this.ensurePackageDirectoryExists(cachePath);
|
|
|
|
|
|
|
|
const requestId = this.installRunCount;
|
|
|
|
this.installRunCount++;
|
|
|
|
|
2016-11-29 19:14:22 +01:00
|
|
|
// send progress event
|
2021-05-18 15:20:57 +02:00
|
|
|
this.sendResponse({
|
2016-11-29 19:14:22 +01:00
|
|
|
kind: EventBeginInstallTypes,
|
|
|
|
eventId: requestId,
|
2019-06-14 11:34:28 +02:00
|
|
|
// qualified explicitly to prevent occasional shadowing
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-qualifier
|
|
|
|
typingsInstallerVersion: ts.version,
|
2016-11-29 19:14:22 +01:00
|
|
|
projectName: req.projectName
|
2021-05-18 15:20:57 +02:00
|
|
|
} as BeginInstallTypes);
|
2016-11-29 19:14:22 +01:00
|
|
|
|
2017-01-30 16:49:17 +01:00
|
|
|
const scopedTypings = filteredTypings.map(typingsName);
|
2016-11-07 22:36:08 +01:00
|
|
|
this.installTypingsAsync(requestId, scopedTypings, cachePath, ok => {
|
2016-11-29 19:14:22 +01:00
|
|
|
try {
|
|
|
|
if (!ok) {
|
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`install request failed, marking packages as missing to prevent repeated requests: ${JSON.stringify(filteredTypings)}`);
|
|
|
|
}
|
|
|
|
for (const typing of filteredTypings) {
|
2020-06-26 19:12:47 +02:00
|
|
|
this.missingTypingsSet.add(typing);
|
2016-11-29 19:14:22 +01:00
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
2016-11-07 22:36:08 +01:00
|
|
|
|
2016-11-29 19:14:22 +01:00
|
|
|
// TODO: watch project directory
|
2016-11-11 01:52:36 +01:00
|
|
|
if (this.log.isEnabled()) {
|
2016-11-29 19:14:22 +01:00
|
|
|
this.log.writeLine(`Installed typings ${JSON.stringify(scopedTypings)}`);
|
2016-11-11 01:52:36 +01:00
|
|
|
}
|
2016-11-29 19:14:22 +01:00
|
|
|
const installedTypingFiles: string[] = [];
|
|
|
|
for (const packageName of filteredTypings) {
|
|
|
|
const typingFile = typingToFileName(cachePath, packageName, this.installTypingHost, this.log);
|
|
|
|
if (!typingFile) {
|
2020-06-26 19:12:47 +02:00
|
|
|
this.missingTypingsSet.add(packageName);
|
2016-11-29 19:14:22 +01:00
|
|
|
continue;
|
|
|
|
}
|
2017-11-28 05:29:10 +01:00
|
|
|
|
2018-01-19 22:13:51 +01:00
|
|
|
// packageName is guaranteed to exist in typesRegistry by filterTypings
|
2018-05-22 23:46:57 +02:00
|
|
|
const distTags = this.typesRegistry.get(packageName)!;
|
2018-08-22 20:22:09 +02:00
|
|
|
const newVersion = new Version(distTags[`ts${versionMajorMinor}`] || distTags[this.latestDistTag]);
|
2018-01-11 20:11:26 +01:00
|
|
|
const newTyping: JsTyping.CachedTyping = { typingLocation: typingFile, version: newVersion };
|
2017-11-28 05:29:10 +01:00
|
|
|
this.packageNameToTypingLocation.set(packageName, newTyping);
|
2016-11-29 19:14:22 +01:00
|
|
|
installedTypingFiles.push(typingFile);
|
2016-08-26 01:25:34 +02:00
|
|
|
}
|
2016-11-29 19:14:22 +01:00
|
|
|
if (this.log.isEnabled()) {
|
|
|
|
this.log.writeLine(`Installed typing files ${JSON.stringify(installedTypingFiles)}`);
|
2016-09-01 06:14:24 +02:00
|
|
|
}
|
2016-11-29 19:14:22 +01:00
|
|
|
|
|
|
|
this.sendResponse(this.createSetTypings(req, currentlyCachedTypings.concat(installedTypingFiles)));
|
2016-08-16 00:59:31 +02:00
|
|
|
}
|
2016-11-29 19:14:22 +01:00
|
|
|
finally {
|
2017-08-10 21:52:15 +02:00
|
|
|
const response: EndInstallTypes = {
|
2016-11-29 19:14:22 +01:00
|
|
|
kind: EventEndInstallTypes,
|
|
|
|
eventId: requestId,
|
|
|
|
projectName: req.projectName,
|
|
|
|
packagesToInstall: scopedTypings,
|
|
|
|
installSuccess: ok,
|
2019-06-14 11:34:28 +02:00
|
|
|
// qualified explicitly to prevent occasional shadowing
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-qualifier
|
|
|
|
typingsInstallerVersion: ts.version
|
2017-08-10 21:52:15 +02:00
|
|
|
};
|
|
|
|
this.sendResponse(response);
|
2016-08-15 20:48:28 +02:00
|
|
|
}
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-11 22:26:44 +01:00
|
|
|
private watchFiles(projectName: string, files: string[], projectRootPath: Path, options: WatchOptions | undefined) {
|
2016-08-16 23:21:09 +02:00
|
|
|
if (!files.length) {
|
2018-03-15 18:44:09 +01:00
|
|
|
// shut down existing watchers
|
|
|
|
this.closeWatchers(projectName);
|
2016-08-16 23:21:09 +02:00
|
|
|
return;
|
|
|
|
}
|
2018-03-15 18:44:09 +01:00
|
|
|
|
2018-05-22 23:46:57 +02:00
|
|
|
let watchers = this.projectWatchers.get(projectName)!;
|
2020-06-26 01:03:25 +02:00
|
|
|
const toRemove = new Map<string, FileWatcher>();
|
2018-03-15 18:44:09 +01:00
|
|
|
if (!watchers) {
|
2020-06-26 01:03:25 +02:00
|
|
|
watchers = new Map();
|
2018-03-15 18:44:09 +01:00
|
|
|
this.projectWatchers.set(projectName, watchers);
|
|
|
|
}
|
2018-04-17 21:18:49 +02:00
|
|
|
else {
|
|
|
|
copyEntries(watchers, toRemove);
|
|
|
|
}
|
2016-08-17 22:01:48 +02:00
|
|
|
|
|
|
|
// handler should be invoked once for the entire set of files since it will trigger full rediscovery of typings
|
2018-04-17 21:18:49 +02:00
|
|
|
watchers.isInvoked = false;
|
|
|
|
|
2018-03-15 18:44:09 +01:00
|
|
|
const isLoggingEnabled = this.log.isEnabled();
|
2018-07-19 22:28:55 +02:00
|
|
|
const createProjectWatcher = (path: string, projectWatcherType: ProjectWatcherType) => {
|
|
|
|
const canonicalPath = this.toCanonicalFileName(path);
|
|
|
|
toRemove.delete(canonicalPath);
|
|
|
|
if (watchers.has(canonicalPath)) {
|
2018-04-17 21:18:49 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isLoggingEnabled) {
|
2018-07-19 22:28:55 +02:00
|
|
|
this.log.writeLine(`${projectWatcherType}:: Added:: WatchInfo: ${path}`);
|
2018-04-17 21:18:49 +02:00
|
|
|
}
|
2018-07-19 22:28:55 +02:00
|
|
|
const watcher = projectWatcherType === ProjectWatcherType.FileWatcher ?
|
2020-11-04 22:30:06 +01:00
|
|
|
this.watchFactory.watchFile(path, () => {
|
2018-07-19 22:28:55 +02:00
|
|
|
if (!watchers.isInvoked) {
|
|
|
|
watchers.isInvoked = true;
|
|
|
|
this.sendResponse({ projectName, kind: ActionInvalidate });
|
|
|
|
}
|
2020-11-04 22:30:06 +01:00
|
|
|
}, PollingInterval.High, options, projectName, watchers) :
|
|
|
|
this.watchFactory.watchDirectory(path, f => {
|
2018-07-19 22:28:55 +02:00
|
|
|
if (watchers.isInvoked || !fileExtensionIs(f, Extension.Json)) {
|
|
|
|
return;
|
|
|
|
}
|
2018-04-17 21:18:49 +02:00
|
|
|
|
2018-07-19 22:28:55 +02:00
|
|
|
if (isPackageOrBowerJson(f, this.installTypingHost.useCaseSensitiveFileNames) &&
|
|
|
|
!sameFiles(f, this.globalCachePackageJsonPath, this.installTypingHost.useCaseSensitiveFileNames)) {
|
|
|
|
watchers.isInvoked = true;
|
|
|
|
this.sendResponse({ projectName, kind: ActionInvalidate });
|
|
|
|
}
|
2020-11-04 22:30:06 +01:00
|
|
|
}, WatchDirectoryFlags.Recursive, options, projectName, watchers);
|
2018-04-17 21:18:49 +02:00
|
|
|
|
2018-07-19 22:28:55 +02:00
|
|
|
watchers.set(canonicalPath, isLoggingEnabled ? {
|
2018-04-17 21:18:49 +02:00
|
|
|
close: () => {
|
2018-07-19 22:28:55 +02:00
|
|
|
this.log.writeLine(`${projectWatcherType}:: Closed:: WatchInfo: ${path}`);
|
2018-04-17 21:18:49 +02:00
|
|
|
watcher.close();
|
|
|
|
}
|
2018-07-19 22:28:55 +02:00
|
|
|
} : watcher);
|
2018-04-17 21:18:49 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
// Create watches from list of files
|
|
|
|
for (const file of files) {
|
2018-07-19 22:28:55 +02:00
|
|
|
if (file.endsWith("/package.json") || file.endsWith("/bower.json")) {
|
2018-04-17 21:18:49 +02:00
|
|
|
// package.json or bower.json exists, watch the file to detect changes and update typings
|
2018-07-19 22:28:55 +02:00
|
|
|
createProjectWatcher(file, ProjectWatcherType.FileWatcher);
|
2018-04-17 21:18:49 +02:00
|
|
|
continue;
|
2018-03-15 18:44:09 +01:00
|
|
|
}
|
2018-04-17 21:18:49 +02:00
|
|
|
|
|
|
|
// path in projectRoot, watch project root
|
2018-07-19 22:28:55 +02:00
|
|
|
if (containsPath(projectRootPath, file, projectRootPath, !this.installTypingHost.useCaseSensitiveFileNames)) {
|
|
|
|
const subDirectory = file.indexOf(directorySeparator, projectRootPath.length + 1);
|
|
|
|
if (subDirectory !== -1) {
|
|
|
|
// Watch subDirectory
|
|
|
|
createProjectWatcher(file.substr(0, subDirectory), ProjectWatcherType.DirectoryWatcher);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
// Watch the directory itself
|
|
|
|
createProjectWatcher(file, ProjectWatcherType.DirectoryWatcher);
|
|
|
|
}
|
2018-04-17 21:18:49 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// path in global cache, watch global cache
|
2018-07-19 22:28:55 +02:00
|
|
|
if (containsPath(this.globalCachePath, file, projectRootPath, !this.installTypingHost.useCaseSensitiveFileNames)) {
|
|
|
|
createProjectWatcher(this.globalCachePath, ProjectWatcherType.DirectoryWatcher);
|
2018-04-17 21:18:49 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-07-19 22:28:55 +02:00
|
|
|
// watch node_modules or bower_components
|
|
|
|
createProjectWatcher(file, ProjectWatcherType.DirectoryWatcher);
|
2018-04-17 21:18:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Remove unused watches
|
|
|
|
toRemove.forEach((watch, path) => {
|
|
|
|
watch.close();
|
|
|
|
watchers.delete(path);
|
|
|
|
});
|
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,
|
2016-11-19 02:46:06 +01:00
|
|
|
typeAcquisition: request.typeAcquisition,
|
2016-08-12 20:04:43 +02:00
|
|
|
compilerOptions: request.compilerOptions,
|
2016-08-16 23:21:09 +02:00
|
|
|
typings,
|
2016-10-26 00:24:21 +02:00
|
|
|
unresolvedImports: request.unresolvedImports,
|
2016-11-10 23:28:34 +01:00
|
|
|
kind: ActionSet
|
2016-08-12 20:04:43 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-10-18 00:04:09 +02:00
|
|
|
private installTypingsAsync(requestId: number, packageNames: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void {
|
|
|
|
this.pendingRunRequests.unshift({ requestId, packageNames, cwd, onRequestCompleted });
|
2016-09-20 23:14:51 +02:00
|
|
|
this.executeWithThrottling();
|
|
|
|
}
|
|
|
|
|
|
|
|
private executeWithThrottling() {
|
|
|
|
while (this.inFlightRequestCount < this.throttleLimit && this.pendingRunRequests.length) {
|
|
|
|
this.inFlightRequestCount++;
|
2018-05-22 23:46:57 +02:00
|
|
|
const request = this.pendingRunRequests.pop()!;
|
2017-10-18 00:04:09 +02:00
|
|
|
this.installWorker(request.requestId, request.packageNames, request.cwd, ok => {
|
2016-09-20 23:14:51 +02:00
|
|
|
this.inFlightRequestCount--;
|
2016-10-13 19:19:18 +02:00
|
|
|
request.onRequestCompleted(ok);
|
2016-09-20 23:14:51 +02:00
|
|
|
this.executeWithThrottling();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-18 00:04:09 +02:00
|
|
|
protected abstract installWorker(requestId: number, packageNames: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void;
|
2016-11-29 19:14:22 +01:00
|
|
|
protected abstract sendResponse(response: SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes): void;
|
2018-06-02 00:52:19 +02:00
|
|
|
|
2018-06-02 01:03:04 +02:00
|
|
|
protected readonly latestDistTag = "latest";
|
2016-08-12 20:04:43 +02:00
|
|
|
}
|
2017-01-30 16:49:17 +01:00
|
|
|
|
|
|
|
/* @internal */
|
|
|
|
export function typingsName(packageName: string): string {
|
|
|
|
return `@types/${packageName}@ts${versionMajorMinor}`;
|
|
|
|
}
|
2018-02-16 23:00:10 +01:00
|
|
|
}
|