TypeScript/src/server/typingsInstaller/typingsInstaller.ts

407 lines
18 KiB
TypeScript
Raw Normal View History

/// <reference path="../../compiler/core.ts" />
/// <reference path="../../compiler/moduleNameResolver.ts" />
2016-08-14 20:16:11 +02:00
/// <reference path="../../services/jsTyping.ts"/>
/// <reference path="../types.d.ts"/>
/// <reference path="../shared.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>;
}
export interface Log {
isEnabled(): boolean;
writeLine(text: string): void;
}
2016-08-17 23:47:54 +02:00
const nullLog: Log = {
isEnabled: () => false,
writeLine: noop
2016-08-17 23:47:54 +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;
}
export enum PackageNameValidationResult {
Ok,
ScopedPackagesNotSupported,
EmptyName,
NameTooLong,
NameStartsWithDot,
NameStartsWithUnderscore,
NameContainsNonURISafeCharacters
}
export const MaxPackageNameLength = 214;
/**
2016-09-26 20:33:25 +02:00
* Validates package name using rules defined at https://docs.npmjs.com/files/package.json
*/
export function validatePackageName(packageName: string): PackageNameValidationResult {
if (!packageName) {
return PackageNameValidationResult.EmptyName;
}
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;
}
export type RequestCompletedAction = (success: boolean) => void;
2016-09-20 23:14:51 +02:00
type PendingRequest = {
requestId: number;
args: string[];
2016-09-20 23:14:51 +02:00
cwd: string;
Merge release-2.1 into master (#12157) * Update LKG * Update version * Update LKG * Skip overloads with too-short function parameters If the parameter of an overload is a function and the argument is also a function, skip the overload if the parameter has fewer arguments than the argument does. That overload cannot possibly apply, and should not participate in, for example, contextual typing. Example: ```ts interface I { (a: number): void; (b: string, c): void; } declare function f(i: I): void; f((x, y) => {}); ``` This code now skips the first overload instead of considering. This was a longstanding bug but was only uncovered now that more functions expressions are context sensitive. * Test skip overloads w/too-short function params 1. Update changed baseline. 2. Add a new test with baseline. * Minor style improvements * Ignore optionality when skipping overloads * Do not use contextual signatures with too few parameters * isAritySmaller runs later: getNonGenericSignature * rewrite void-returning statements in constructors that capture result of super call (#11868) * rewrite void-returning statements in constructors that capture result of super call * linter * Update LKG * Fix emit inferred type which is a generic type-alias both fully and partially fill type parameters * Add tests and baselines * Skip trying to use alias if there is target type * Update baselines * Add diagnostics to remind adding tsconfig file for certain external project (#11932) * Add diagnostics for certain external project * Show tsconfig suggestion * fix lint error * Address pr * fix comment * Update error message * Flag for not overwrite js files by default without generating errors (#11980) * WIP * Properly naming things * refactor * apply the option to all files and check out options * Fix typo * Update LKG * lockLinter * use local registry to check if typings package exist (#12014) (#12032) use local registry to check if typings package exist * Add test for https://github.com/Microsoft/TypeScript/pull/11980 (#12027) * add test for the fix for overwrite emitting error * cr feedback * enable sending telemetry events to tsserver client (#12034) (#12051) enable sending telemetry events * Update LKG * Reuse subtree transform flags for incrementally parsed nodes (#12088) * Update LKG * Update version * Update LKG * Do not emit "use strict" when targeting es6 or higher or module kind is es2015 and the file is external module * Add tests and baselines * [Release 2.1] fix11754 global augmentation (#12133) * Exclude global augmentation from module resolution logic * Address PR: check using string literal instead of NodeFlags.globalAugmentation * Remove comment
2016-11-10 23:28:34 +01:00
onRequestCompleted: RequestCompletedAction;
2016-09-20 23:14:51 +02:00
};
2016-08-12 20:04:43 +02:00
export abstract class TypingsInstaller {
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[]>();
2016-09-20 23:14:51 +02:00
readonly pendingRunRequests: PendingRequest[] = [];
private installRunCount = 1;
2016-09-20 23:14:51 +02:00
private inFlightRequestCount = 0;
2016-08-16 23:21:09 +02:00
abstract readonly typesRegistry: Map<void>;
2016-09-20 23:14:51 +02:00
constructor(
readonly installTypingHost: InstallTypingHost,
2016-09-20 23:14:51 +02:00
readonly globalCachePath: string,
readonly safeListPath: Path,
readonly throttleLimit: number,
readonly telemetryEnabled: boolean,
2016-09-20 23:14:51 +02:00
protected readonly log = nullLog) {
if (this.log.isEnabled()) {
this.log.writeLine(`Global cache location '${globalCachePath}', safe file path '${safeListPath}'`);
}
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): 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];
2016-08-16 23:21:09 +02:00
if (!watchers) {
if (this.log.isEnabled()) {
this.log.writeLine(`No watchers are registered for project '${projectName}'`);
}
return;
2016-08-16 23:21:09 +02:00
}
for (const w of watchers) {
w.close();
}
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) {
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
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,
req.typingOptions,
req.unresolvedImports);
2016-08-17 23:47:54 +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-12 21:14:25 +02:00
// start watching files
this.watchFiles(req.projectName, discoverTypingsResult.filesToWatch);
// install typings
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
}
private processCacheLocation(cacheLocation: string) {
if (this.log.isEnabled()) {
this.log.writeLine(`Processing cache location '${cacheLocation}'`);
}
if (this.knownCachesSet[cacheLocation]) {
if (this.log.isEnabled()) {
2016-08-17 23:47:54 +02:00
this.log.writeLine(`Cache location was already processed...`);
}
return;
}
2016-08-26 01:25:34 +02:00
const packageJson = combinePaths(cacheLocation, "package.json");
if (this.log.isEnabled()) {
2016-08-26 01:25:34 +02:00
this.log.writeLine(`Trying to find '${packageJson}'...`);
}
2016-08-26 01:25:34 +02:00
if (this.installTypingHost.fileExists(packageJson)) {
const npmConfig = <NpmConfig>JSON.parse(this.installTypingHost.readFile(packageJson));
if (this.log.isEnabled()) {
this.log.writeLine(`Loaded content of '${packageJson}': ${JSON.stringify(npmConfig)}`);
}
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);
if (!packageName) {
continue;
}
const typingFile = typingToFileName(cacheLocation, packageName, this.installTypingHost);
2016-08-26 01:25:34 +02:00
if (!typingFile) {
continue;
}
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;
}
private filterAndMapToScopedName(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) {
if (typing in this.typesRegistry) {
result.push(`@types/${typing}`);
}
else {
if (this.log.isEnabled()) {
this.log.writeLine(`Entry for package '${typing}' does not exist in local types registry - skipping...`);
}
}
}
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.EmptyName:
this.log.writeLine(`Package name '${typing}' cannot be empty`);
break;
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;
}
protected ensurePackageDirectoryExists(directory: string) {
const npmConfigPath = combinePaths(directory, "package.json");
if (this.log.isEnabled()) {
this.log.writeLine(`Npm config file: ${npmConfigPath}`);
}
if (!this.installTypingHost.fileExists(npmConfigPath)) {
if (this.log.isEnabled()) {
this.log.writeLine(`Npm config file: '${npmConfigPath}' is missing, creating new one...`);
}
this.ensureDirectoryExists(directory, this.installTypingHost);
this.installTypingHost.writeFile(npmConfigPath, "{}");
2016-08-12 21:14:25 +02:00
}
}
2016-08-12 21:14:25 +02:00
private installTypings(req: DiscoverTypings, cachePath: string, currentlyCachedTypings: string[], typingsToInstall: string[]) {
if (this.log.isEnabled()) {
this.log.writeLine(`Installing typings ${JSON.stringify(typingsToInstall)}`);
}
const scopedTypings = this.filterAndMapToScopedName(typingsToInstall);
if (scopedTypings.length === 0) {
if (this.log.isEnabled()) {
this.log.writeLine(`All typings are known to be missing or invalid - no need to go any further`);
}
return;
2016-08-12 20:04:43 +02:00
}
2016-08-12 21:14:25 +02:00
this.ensurePackageDirectoryExists(cachePath);
const requestId = this.installRunCount;
this.installRunCount++;
this.installTypingsAsync(requestId, scopedTypings, cachePath, ok => {
if (this.telemetryEnabled) {
this.sendResponse(<TypingsInstallEvent>{
kind: EventInstall,
packagesToInstall: scopedTypings
});
}
if (!ok) {
return;
}
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(scopedTypings)}, installed typings ${JSON.stringify(scopedTypings)}`);
}
const installedTypingFiles: string[] = [];
for (const t of scopedTypings) {
2016-08-27 01:37:31 +02:00
const packageName = getBaseFileName(t);
if (!packageName) {
continue;
}
const typingFile = typingToFileName(cachePath, packageName, this.installTypingHost);
2016-08-26 01:25:34 +02:00
if (!typingFile) {
continue;
}
if (!this.packageNameToTypingLocation[packageName]) {
this.packageNameToTypingLocation[packageName] = typingFile;
2016-09-01 06:14:24 +02:00
}
2016-08-26 01:25:34 +02:00
installedTypingFiles.push(typingFile);
}
if (this.log.isEnabled()) {
this.log.writeLine(`Installed typing files ${JSON.stringify(installedTypingFiles)}`);
}
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;
}
// 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()) {
this.log.writeLine(`Got FS notification for ${f}, handler is already invoked '${isInvoked}'`);
2016-08-16 23:21:09 +02:00
}
if (!isInvoked) {
this.sendResponse({ projectName: projectName, kind: server.ActionInvalidate });
isInvoked = true;
}
}, /*pollingInterval*/ 2000);
2016-08-16 23:21:09 +02:00
watchers.push(w);
}
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,
unresolvedImports: request.unresolvedImports,
Merge release-2.1 into master (#12157) * Update LKG * Update version * Update LKG * Skip overloads with too-short function parameters If the parameter of an overload is a function and the argument is also a function, skip the overload if the parameter has fewer arguments than the argument does. That overload cannot possibly apply, and should not participate in, for example, contextual typing. Example: ```ts interface I { (a: number): void; (b: string, c): void; } declare function f(i: I): void; f((x, y) => {}); ``` This code now skips the first overload instead of considering. This was a longstanding bug but was only uncovered now that more functions expressions are context sensitive. * Test skip overloads w/too-short function params 1. Update changed baseline. 2. Add a new test with baseline. * Minor style improvements * Ignore optionality when skipping overloads * Do not use contextual signatures with too few parameters * isAritySmaller runs later: getNonGenericSignature * rewrite void-returning statements in constructors that capture result of super call (#11868) * rewrite void-returning statements in constructors that capture result of super call * linter * Update LKG * Fix emit inferred type which is a generic type-alias both fully and partially fill type parameters * Add tests and baselines * Skip trying to use alias if there is target type * Update baselines * Add diagnostics to remind adding tsconfig file for certain external project (#11932) * Add diagnostics for certain external project * Show tsconfig suggestion * fix lint error * Address pr * fix comment * Update error message * Flag for not overwrite js files by default without generating errors (#11980) * WIP * Properly naming things * refactor * apply the option to all files and check out options * Fix typo * Update LKG * lockLinter * use local registry to check if typings package exist (#12014) (#12032) use local registry to check if typings package exist * Add test for https://github.com/Microsoft/TypeScript/pull/11980 (#12027) * add test for the fix for overwrite emitting error * cr feedback * enable sending telemetry events to tsserver client (#12034) (#12051) enable sending telemetry events * Update LKG * Reuse subtree transform flags for incrementally parsed nodes (#12088) * Update LKG * Update version * Update LKG * Do not emit "use strict" when targeting es6 or higher or module kind is es2015 and the file is external module * Add tests and baselines * [Release 2.1] fix11754 global augmentation (#12133) * Exclude global augmentation from module resolution logic * Address PR: check using string literal instead of NodeFlags.globalAugmentation * Remove comment
2016-11-10 23:28:34 +01:00
kind: ActionSet
2016-08-12 20:04:43 +02:00
};
}
private installTypingsAsync(requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void {
this.pendingRunRequests.unshift({ requestId, args, cwd, onRequestCompleted });
2016-09-20 23:14:51 +02:00
this.executeWithThrottling();
}
private executeWithThrottling() {
while (this.inFlightRequestCount < this.throttleLimit && this.pendingRunRequests.length) {
this.inFlightRequestCount++;
const request = this.pendingRunRequests.pop();
this.installWorker(request.requestId, request.args, request.cwd, ok => {
2016-09-20 23:14:51 +02:00
this.inFlightRequestCount--;
request.onRequestCompleted(ok);
2016-09-20 23:14:51 +02:00
this.executeWithThrottling();
});
}
}
protected abstract installWorker(requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void;
protected abstract sendResponse(response: SetTypings | InvalidateCachedTypings | TypingsInstallEvent): void;
2016-08-12 20:04:43 +02:00
}
}