/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
vscode-ng-language-service/server/src/server_host.ts
289 строк
8 KB
Andrew Scott
feat(language-server): Support client-side file watching via `onDidChangeWatchedFiles`
23 янв 2026, 22:52
23 янв 2026, 22:52
6fb39d9
Код
Авторство
О чём код?
/*! * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import * as ts from 'typescript/lib/tsserverlibrary'; import * as path from 'path'; import * as lsp from 'vscode-languageserver/node'; const NOOP_WATCHER: ts.FileWatcher = { close() {}, }; /** * `ServerHost` is a wrapper around `ts.sys` for the Node system. In Node, all * optional methods of `ts.System` are implemented. * See * https://github.com/microsoft/TypeScript/blob/ec39d412876d0dcf704fc886d5036cb625220d2f/src/compiler/sys.ts#L716 */ export class ServerHost implements ts.server.ServerHost { readonly args: string[]; readonly newLine: string; readonly useCaseSensitiveFileNames: boolean; private readonly fileWatchers = new Map<string, Set<ts.FileWatcherCallback>>(); private readonly directoryWatchers = new Map< string, Set<{callback: ts.DirectoryWatcherCallback; recursive: boolean}> >(); constructor( readonly isG3: boolean, private readonly useClientSideFileWatcher: boolean, ) { this.args = ts.sys.args; this.newLine = ts.sys.newLine; this.useCaseSensitiveFileNames = ts.sys.useCaseSensitiveFileNames; } write(s: string): void { ts.sys.write(s); } writeOutputIsTTY(): boolean { return ts.sys.writeOutputIsTTY!(); } readFile(path: string, encoding?: string): string | undefined { return ts.sys.readFile(path, encoding); } getFileSize(path: string): number { return ts.sys.getFileSize!(path); } writeFile(path: string, data: string, writeByteOrderMark?: boolean): void { return ts.sys.writeFile(path, data, writeByteOrderMark); } /** * @pollingInterval - this parameter is used in polling-based watchers and * ignored in watchers that use native OS file watching */ watchFile( path: string, callback: ts.FileWatcherCallback, pollingInterval?: number, options?: ts.WatchOptions, ): ts.FileWatcher { if (!this.useClientSideFileWatcher) { return ts.sys.watchFile!(path, callback, pollingInterval, options); } const callbacks = this.fileWatchers.get(path) ?? new Set(); callbacks.add(callback); this.fileWatchers.set(path, callbacks); return { close: () => { const callbacks = this.fileWatchers.get(path); if (callbacks) { callbacks.delete(callback); if (callbacks.size === 0) { this.fileWatchers.delete(path); } } }, }; } watchDirectory( path: string, callback: ts.DirectoryWatcherCallback, recursive?: boolean, options?: ts.WatchOptions, ): ts.FileWatcher { if (this.isG3 && path.startsWith('/google/src')) { return NOOP_WATCHER; } if (!this.useClientSideFileWatcher) { return ts.sys.watchDirectory!(path, callback, recursive, options); } const callbacks = this.directoryWatchers.get(path) ?? new Set(); const watcher = {callback, recursive: !!recursive}; callbacks.add(watcher); this.directoryWatchers.set(path, callbacks); return { close: () => { const callbacks = this.directoryWatchers.get(path); if (callbacks) { callbacks.delete(watcher); if (callbacks.size === 0) { this.directoryWatchers.delete(path); } } }, }; } notifyFileChange(fileName: string, type: lsp.FileChangeType): void { if (!this.useClientSideFileWatcher) { return; } const callbacks = this.fileWatchers.get(fileName); if (callbacks) { callbacks.forEach((callback) => callback( fileName, type === lsp.FileChangeType.Deleted ? ts.FileWatcherEventKind.Deleted : ts.FileWatcherEventKind.Changed, ), ); } for (const [dirPath, watchers] of this.directoryWatchers) { if (fileName.startsWith(dirPath)) { // If it's a direct child or recursive watch const relative = path.relative(dirPath, fileName); const isDirectChild = !relative.includes(path.sep); for (const watcher of watchers) { if (watcher.recursive || isDirectChild) { watcher.callback(fileName); } } } } } resolvePath(path: string): string { return ts.sys.resolvePath(path); } fileExists(path: string): boolean { // When a project is reloaded (due to changes in node_modules for example), // the typecheck files ought to be retained. However, if they do not exist // on disk, tsserver will remove them from project. See // https://github.com/microsoft/TypeScript/blob/3c32f6e154ead6749b76ec9c19cbfdd2acad97d6/src/server/editorServices.ts#L2188-L2193 // To fix this, we fake the existence of the typecheck files. if (path.endsWith('.ngtypecheck.ts')) { return true; } return ts.sys.fileExists(path); } directoryExists(path: string): boolean { return ts.sys.directoryExists(path); } createDirectory(path: string): void { return ts.sys.createDirectory(path); } getExecutingFilePath(): string { return ts.sys.getExecutingFilePath(); } getCurrentDirectory(): string { return ts.sys.getCurrentDirectory(); } getDirectories(path: string): string[] { return ts.sys.getDirectories(path); } readDirectory( path: string, extensions?: ReadonlyArray<string>, exclude?: ReadonlyArray<string>, include?: ReadonlyArray<string>, depth?: number, ): string[] { return ts.sys.readDirectory(path, extensions, exclude, include, depth); } getModifiedTime(path: string): Date | undefined { return ts.sys.getModifiedTime!(path); } setModifiedTime(path: string, time: Date): void { return ts.sys.setModifiedTime!(path, time); } deleteFile(path: string): void { return ts.sys.deleteFile!(path); } /** * A good implementation is node.js' `crypto.createHash`. * (https://nodejs.org/api/crypto.html#crypto_crypto_createhash_algorithm) */ createHash(data: string): string { return ts.sys.createHash!(data); } /** * This must be cryptographically secure. Only implement this method using * `crypto.createHash("sha256")`. */ createSHA256Hash(data: string): string { return ts.sys.createSHA256Hash!(data); } getMemoryUsage(): number { return ts.sys.getMemoryUsage!(); } exit(exitCode?: number): void { return ts.sys.exit(exitCode); } realpath(path: string): string { return ts.sys.realpath!(path); } setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any { return ts.sys.setTimeout!(callback, ms, ...args); } clearTimeout(timeoutId: any): void { return ts.sys.clearTimeout!(timeoutId); } clearScreen(): void { return ts.sys.clearScreen!(); } base64decode(input: string): string { return ts.sys.base64decode!(input); } base64encode(input: string): string { return ts.sys.base64encode!(input); } setImmediate(callback: (...args: any[]) => void, ...args: any[]): any { return setImmediate(callback, ...args); } clearImmediate(timeoutId: any): void { return clearImmediate(timeoutId); } require(initialPath: string, moduleName: string): ts.server.RequireResult { if (moduleName !== '@angular/language-service') { return { module: undefined, error: new Error(`Angular server will not load plugin '${moduleName}'.`), }; } try { const modulePath = require.resolve(moduleName, { paths: [initialPath], }); return { module: require(modulePath), error: undefined, }; } catch (e) { return { module: undefined, error: e as Error, }; } } }