import { URI } from "vscode-uri"; import { GraphQLSchema } from "graphql"; import { NotificationHandler, PublishDiagnosticsParams, CancellationToken, SymbolInformation, Connection, ServerRequestHandler, TextDocumentChangeEvent, StarRequestHandler, StarNotificationHandler, ServerCapabilities, } from "vscode-languageserver/node"; import { TextDocument } from "vscode-languageserver-textdocument"; import type { LoadingHandler } from "../loadingHandler"; import { FileSet } from "../fileSet"; import { ApolloConfig, ClientConfig, envFileNames, RoverConfig, supportedConfigFileNames, } from "../config"; import type { ProjectStats } from "../../messages"; export type DocumentUri = string; export interface GraphQLProjectConfig { config: ClientConfig | RoverConfig; configFolderURI: URI; loadingHandler: LoadingHandler; } type ConnectionHandler = { [K in keyof Connection as K extends `on${string}` ? K : never]: Connection[K] extends ( params: ServerRequestHandler & infer P, token: CancellationToken, ) => any ? P : never; }; export abstract class GraphQLProject { protected _onDiagnostics?: NotificationHandler; private _isReady: boolean; private readyPromise: Promise; public config: ApolloConfig; protected schema?: GraphQLSchema; protected rootURI: URI; protected loadingHandler: LoadingHandler; protected lastLoadDate?: number; private configFileSet: FileSet; constructor({ config, configFolderURI, loadingHandler, }: GraphQLProjectConfig) { this.config = config; this.loadingHandler = loadingHandler; // the URI of the folder _containing_ the apollo.config.js is the true project's root. // if a config doesn't have a uri associated, we can assume the `rootURI` is the project's root. this.rootURI = config.configDirURI || configFolderURI; this.configFileSet = new FileSet({ rootURI: this.rootURI, includes: supportedConfigFileNames.concat(envFileNames), excludes: [], }); this._isReady = false; this.readyPromise = Promise.resolve() .then( // FIXME: Instead of `Promise.all`, we should catch individual promise rejections // so we can show multiple errors. () => Promise.all(this.initialize()), ) .then(() => { this._isReady = true; }) .catch((error) => { console.error(error); this.loadingHandler.showError( `Error initializing Apollo GraphQL project "${this.displayName}": ${error}`, ); }); } abstract get displayName(): string; abstract initialize(): Promise[]; abstract getProjectStats(): ProjectStats; get isReady(): boolean { return this._isReady; } get whenReady(): Promise { return this.readyPromise; } public updateConfig(config: ApolloConfig) { this.config = config; return this.initialize(); } onDiagnostics(handler: NotificationHandler) { this._onDiagnostics = handler; } abstract includesFile(uri: DocumentUri, languageId?: string): boolean; isConfiguredBy(uri: DocumentUri): boolean { return this.configFileSet.includesFile(uri); } abstract onDidChangeWatchedFiles: ConnectionHandler["onDidChangeWatchedFiles"]; onDidOpen?: (event: TextDocumentChangeEvent) => void; onDidClose?: (event: TextDocumentChangeEvent) => void; abstract documentDidChange(document: TextDocument): void; abstract clearAllDiagnostics(): void; onCompletion?: ConnectionHandler["onCompletion"]; onHover?: ConnectionHandler["onHover"]; onDefinition?: ConnectionHandler["onDefinition"]; onReferences?: ConnectionHandler["onReferences"]; onDocumentSymbol?: ConnectionHandler["onDocumentSymbol"]; onCodeLens?: ConnectionHandler["onCodeLens"]; onCodeAction?: ConnectionHandler["onCodeAction"]; onUnhandledRequest?: StarRequestHandler; onUnhandledNotification?: ( connection: Connection, ...rest: Parameters ) => ReturnType; dispose?(): void; provideSymbol?( query: string, token: CancellationToken, ): Promise; onVSCodeConnectionInitialized?(connection: Connection): void; validate?(): void; }