diff --git a/src/compiler/extensions.ts b/src/compiler/extensions.ts index a458b0a247a4f..1c344a0a68f66 100644 --- a/src/compiler/extensions.ts +++ b/src/compiler/extensions.ts @@ -95,7 +95,6 @@ namespace ts { export interface BaseProviderStatic { readonly ["extension-kind"]: ExtensionKind; - new (state: {ts: typeof ts, args: any}): any; } export interface SyntacticLintProviderStatic extends BaseProviderStatic { @@ -125,13 +124,16 @@ namespace ts { export type SyntacticLint = "syntactic-lint"; export const LanguageService: "language-service" = "language-service"; export type LanguageService = "language-service"; + export const TypeDiscovery: "type-discovery" = "type-discovery"; + export type TypeDiscovery = "type-discovery"; } - export type ExtensionKind = ExtensionKind.SemanticLint | ExtensionKind.SyntacticLint | ExtensionKind.LanguageService; + export type ExtensionKind = ExtensionKind.SemanticLint | ExtensionKind.SyntacticLint | ExtensionKind.LanguageService | ExtensionKind.TypeDiscovery; export interface ExtensionCollectionMap { "syntactic-lint"?: SyntacticLintExtension[]; "semantic-lint"?: SemanticLintExtension[]; "language-service"?: LanguageServiceExtension[]; + "type-discovery"?: TypeDiscoveryExtension[]; [index: string]: Extension[] | undefined; } @@ -163,7 +165,12 @@ namespace ts { ctor: LanguageServiceProviderStatic; } - export type Extension = SyntacticLintExtension | SemanticLintExtension | LanguageServiceExtension; + export interface TypeDiscoveryExtension extends ExtensionBase { + kind: ExtensionKind.TypeDiscovery; + lookup: (searchDir: string, args: any) => string[]; + } + + export type Extension = SyntacticLintExtension | SemanticLintExtension | LanguageServiceExtension | TypeDiscoveryExtension; export interface ExtensionCache { getCompilerExtensions(): ExtensionCollectionMap; @@ -226,6 +233,21 @@ namespace ts { completeProfile(/*enabled*/true, longTask); } + function verifyType(thing: any, type: string, diagnostics: Diagnostic[], extName: string, extMember: string, extKind: ExtensionKind) { + if (typeof thing !== type) { + diagnostics.push(createCompilerDiagnostic( + Diagnostics.Extension_0_exported_member_1_has_extension_kind_2_but_was_type_3_when_type_4_was_expected, + extName, + extMember, + extKind, + typeof thing, + type + )); + return false; + } + return true; + } + export function createExtensionCache(options: CompilerOptions, host: ExtensionHost, resolvedExtensionNames?: Map): ExtensionCache { const diagnostics: Diagnostic[] = []; @@ -312,20 +334,18 @@ namespace ts { switch (ext.kind) { case ExtensionKind.SemanticLint: case ExtensionKind.SyntacticLint: - case ExtensionKind.LanguageService: - if (typeof potentialExtension !== "function") { - diagnostics.push(createCompilerDiagnostic( - Diagnostics.Extension_0_exported_member_1_has_extension_kind_2_but_was_type_3_when_type_4_was_expected, - res.name, - key, - (ts as any).ExtensionKind[annotatedKind], - typeof potentialExtension, - "function" - )); - return; - } + case ExtensionKind.LanguageService: { + const verified = verifyType(potentialExtension, "function", diagnostics, res.name, key, annotatedKind); + if (!verified) return; (ext as (SemanticLintExtension | SyntacticLintExtension | LanguageServiceExtension)).ctor = potentialExtension as (SemanticLintProviderStatic | SyntacticLintProviderStatic | LanguageServiceProviderStatic); break; + } + case ExtensionKind.TypeDiscovery: { + const verified = verifyType(potentialExtension, "function", diagnostics, res.name, key, annotatedKind); + if (!verified) return; + (ext as TypeDiscoveryExtension).lookup = potentialExtension as any; + break; + } default: // Include a default case which just puts the extension unchecked onto the base extension // This can allow language service extensions to query for custom extension kinds diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 8ddc3f5ea865a..61adadc6c7aa5 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -1526,7 +1526,7 @@ namespace ts { * ExtensionKind.SyntacticLint or ExtensionKind.SemanticLint only */ function performLintPassOnFile(sourceFile: SourceFile, kind: ExtensionKind): Diagnostic[] | undefined { - const lints = extensionCache.getCompilerExtensions()[kind]; + const lints = extensionCache.getCompilerExtensions()[kind] as (SemanticLintExtension | SyntacticLintExtension)[]; if (!lints || !lints.length) { return; } diff --git a/src/services/jsTyping.ts b/src/services/jsTyping.ts index 3b013a4a924ac..7e63e20720c8a 100644 --- a/src/services/jsTyping.ts +++ b/src/services/jsTyping.ts @@ -6,7 +6,7 @@ /* @internal */ namespace ts.JsTyping { - export interface TypingResolutionHost { + export interface TypingResolutionHost extends ExtensionHost { directoryExists: (path: string) => boolean; fileExists: (fileName: string) => boolean; readFile: (path: string, encoding?: string) => string; @@ -35,6 +35,7 @@ namespace ts.JsTyping { * @param packageNameToTypingLocation is the map of package names to their cached typing locations * @param typingOptions are used to customize the typing inference process * @param compilerOptions are used as a source for typing inference + * @param cache is the optional extension cache to lookup type discovery extensions in - one will be made if it cannot be provided */ export function discoverTypings( host: TypingResolutionHost, @@ -43,7 +44,8 @@ namespace ts.JsTyping { safeListPath: Path, packageNameToTypingLocation: Map, typingOptions: TypingOptions, - compilerOptions: CompilerOptions): + compilerOptions: CompilerOptions, + cache: ExtensionCache = createExtensionCache(compilerOptions, host)): { cachedTypingPaths: string[], newTypingNames: string[], filesToWatch: string[] } { // A typing name to typing file path mapping @@ -88,6 +90,8 @@ namespace ts.JsTyping { const nodeModulesPath = combinePaths(searchDir, "node_modules"); getTypingNamesFromNodeModuleFolder(nodeModulesPath); + + getTypingNamesFromExtensions(searchDir, cache); } getTypingNamesFromSourceFileNames(fileNames); @@ -223,5 +227,24 @@ namespace ts.JsTyping { mergeTypings(typingNames); } + + function getTypingNamesFromExtensions(searchPath: string, cache: ExtensionCache) { + // We don't report issues loading type discovery extensions (if the host cares about them, it should load them in advance and pass in a cache) + const extensions = cache.getCompilerExtensions()["type-discovery"]; + for (const extension of extensions) { + let results: string[]; + try { + startExtensionProfile(compilerOptions.extendedDiagnostics, extension.name, "lookup"); + results = extension.lookup(searchPath, extension.args); + completeExtensionProfile(compilerOptions.extendedDiagnostics, extension.name, "lookup"); + } + catch (e) { + // There's presently no way to report errors during discover typings other than a hard failure (which is to be avoided) + } + if (results) { + mergeTypings(results); + } + } + } } }