diff --git a/packages/language-server/lib/hybridModeProject.ts b/packages/language-server/lib/hybridModeProject.ts index 5a606bd530..e9747126bd 100644 --- a/packages/language-server/lib/hybridModeProject.ts +++ b/packages/language-server/lib/hybridModeProject.ts @@ -2,7 +2,7 @@ import type { Language, LanguagePlugin, LanguageServer, LanguageServerProject, P import { createLanguageServiceEnvironment } from '@volar/language-server/lib/project/simpleProject'; import { createLanguage } from '@vue/language-core'; import { createLanguageService, createUriMap, LanguageService } from '@vue/language-service'; -import { getReadyNamedPipePaths, onSomePipeReadyCallbacks, searchNamedPipeServerForFile } from '@vue/typescript-plugin/lib/utils'; +import { configuredServers, getBestServer, inferredServers, onServerReady } from '@vue/typescript-plugin/lib/utils'; import { URI } from 'vscode-uri'; export function createHybridModeProject( @@ -24,7 +24,7 @@ export function createHybridModeProject( const project: LanguageServerProject = { setup(_server) { server = _server; - onSomePipeReadyCallbacks.push(() => { + onServerReady.push(() => { server.languageFeatures.requestRefresh(false); }); server.fileWatcher.onDidChangeWatchedFiles(({ changes }) => { @@ -38,16 +38,20 @@ export function createHybridModeProject( }); const end = Date.now() + 60000; const pipeWatcher = setInterval(() => { - getReadyNamedPipePaths(); + for (const server of configuredServers) { + server.update(); + } + for (const server of inferredServers) { + server.update(); + } if (Date.now() > end) { clearInterval(pipeWatcher); } - }, 1000); + }, 2500); }, async getLanguageService(uri) { const fileName = asFileName(uri); - const namedPipeServer = (await searchNamedPipeServerForFile(fileName)); - namedPipeServer?.socket.end(); + const namedPipeServer = await getBestServer(fileName); if (namedPipeServer?.projectInfo?.kind === 1) { const tsconfig = namedPipeServer.projectInfo.name; const tsconfigUri = URI.file(tsconfig); diff --git a/packages/language-service/lib/plugins/vue-template.ts b/packages/language-service/lib/plugins/vue-template.ts index ae6f2bbf7f..54f4db2638 100644 --- a/packages/language-service/lib/plugins/vue-template.ts +++ b/packages/language-service/lib/plugins/vue-template.ts @@ -242,7 +242,9 @@ export function create( ? tagName : components.find(component => component === tagName || hyphenateTag(component) === tagName); if (checkTag) { - componentProps[checkTag] ??= (await tsPluginClient?.getComponentProps(code.fileName, checkTag, true) ?? []).map(prop => prop.name); + componentProps[checkTag] ??= (await tsPluginClient?.getComponentProps(code.fileName, checkTag) ?? []) + .filter(prop => prop.required) + .map(prop => prop.name); current = { unburnedRequiredProps: [...componentProps[checkTag]], labelOffset: scanner.getTokenOffset() + scanner.getTokenLength(), @@ -469,7 +471,7 @@ export function create( const promises: Promise[] = []; const tagInfos = new Map(); @@ -1010,7 +1012,7 @@ function parseLabel(label: string) { return { name, leadingSlash - } + }; } function generateItemKey(type: InternalItemId, tag: string, prop: string) { diff --git a/packages/typescript-plugin/lib/client.ts b/packages/typescript-plugin/lib/client.ts index ce8d82db99..04a5f777b1 100644 --- a/packages/typescript-plugin/lib/client.ts +++ b/packages/typescript-plugin/lib/client.ts @@ -1,96 +1,99 @@ -import type { Request } from './server'; -import { searchNamedPipeServerForFile, sendRequestWorker } from './utils'; +import type { RequestData } from './server'; +import { getBestServer } from './utils'; export function collectExtractProps( ...args: Parameters ) { - return sendRequest>({ - type: 'collectExtractProps', - args, - }); + return sendRequest>( + 'collectExtractProps', + ...args + ); } export async function getImportPathForFile( ...args: Parameters ) { - return await sendRequest>({ - type: 'getImportPathForFile', - args, - }); + return await sendRequest>( + 'getImportPathForFile', + ...args + ); } export async function getPropertiesAtLocation( ...args: Parameters ) { - return await sendRequest>({ - type: 'getPropertiesAtLocation', - args, - }); + return await sendRequest>( + 'getPropertiesAtLocation', + ...args + ); } export function getQuickInfoAtPosition( ...args: Parameters ) { - return sendRequest>({ - type: 'getQuickInfoAtPosition', - args, - }); + return sendRequest>( + 'getQuickInfoAtPosition', + ...args + ); } // Component Infos -export function getComponentProps( - ...args: Parameters -) { - return sendRequest>({ - type: 'getComponentProps', - args, - }); +export async function getComponentProps(fileName: string, componentName: string) { + const server = await getBestServer(fileName); + if (!server) { + return; + } + const componentAndProps = await server.componentNamesAndProps.get(fileName); + if (!componentAndProps) { + return; + } + return componentAndProps[componentName]; } export function getComponentEvents( ...args: Parameters ) { - return sendRequest>({ - type: 'getComponentEvents', - args, - }); + return sendRequest>( + 'getComponentEvents', + ...args + ); } export function getTemplateContextProps( ...args: Parameters ) { - return sendRequest>({ - type: 'getTemplateContextProps', - args, - }); + return sendRequest>( + 'getTemplateContextProps', + ...args + ); } -export function getComponentNames( - ...args: Parameters -) { - return sendRequest>({ - type: 'getComponentNames', - args, - }); +export async function getComponentNames(fileName: string) { + const server = await getBestServer(fileName); + if (!server) { + return; + } + const componentAndProps = server.componentNamesAndProps.get(fileName); + if (!componentAndProps) { + return; + } + return Object.keys(componentAndProps); } export function getElementAttrs( ...args: Parameters ) { - return sendRequest>({ - type: 'getElementAttrs', - args, - }); + return sendRequest>( + 'getElementAttrs', + ...args + ); } -async function sendRequest(request: Request) { - const server = (await searchNamedPipeServerForFile(request.args[0])); +async function sendRequest(requestType: RequestData[1], fileName: string, ...rest: any[]) { + const server = await getBestServer(fileName); if (!server) { - console.warn('[Vue Named Pipe Client] No server found for', request.args[0]); return; } - const res = await sendRequestWorker(request, server.socket); - server.socket.end(); - return res; + return server.request(requestType, fileName, ...rest); } diff --git a/packages/typescript-plugin/lib/requests/componentInfos.ts b/packages/typescript-plugin/lib/requests/componentInfos.ts index c345b33cc0..d1fa344292 100644 --- a/packages/typescript-plugin/lib/requests/componentInfos.ts +++ b/packages/typescript-plugin/lib/requests/componentInfos.ts @@ -6,8 +6,7 @@ import type { RequestContext } from './types'; export function getComponentProps( this: RequestContext, fileName: string, - tag: string, - requiredOnly = false + tag: string ) { const { typescript: ts, language, languageService, getFileId } = this; const volarFile = language.scripts.get(getFileId(fileName)); @@ -47,7 +46,11 @@ export function getComponentProps( } } - const result = new Map(); + const result = new Map(); for (const sig of componentType.getCallSignatures()) { const propParam = sig.parameters[0]; @@ -55,12 +58,11 @@ export function getComponentProps( const propsType = checker.getTypeOfSymbolAtLocation(propParam, components.node); const props = propsType.getProperties(); for (const prop of props) { - if (!requiredOnly || !(prop.flags & ts.SymbolFlags.Optional)) { - const name = prop.name; - const commentMarkdown = generateCommentMarkdown(prop.getDocumentationComment(checker), prop.getJsDocTags()); + const name = prop.name; + const required = !(prop.flags & ts.SymbolFlags.Optional) || undefined; + const commentMarkdown = generateCommentMarkdown(prop.getDocumentationComment(checker), prop.getJsDocTags()) || undefined; - result.set(name, { name, commentMarkdown }); - } + result.set(name, { name, required, commentMarkdown }); } } } @@ -75,12 +77,11 @@ export function getComponentProps( if (prop.flags & ts.SymbolFlags.Method) { // #2443 continue; } - if (!requiredOnly || !(prop.flags & ts.SymbolFlags.Optional)) { - const name = prop.name; - const commentMarkdown = generateCommentMarkdown(prop.getDocumentationComment(checker), prop.getJsDocTags()); + const name = prop.name; + const required = !(prop.flags & ts.SymbolFlags.Optional) || undefined; + const commentMarkdown = generateCommentMarkdown(prop.getDocumentationComment(checker), prop.getJsDocTags()) || undefined; - result.set(name, { name, commentMarkdown }); - } + result.set(name, { name, required, commentMarkdown }); } } } diff --git a/packages/typescript-plugin/lib/server.ts b/packages/typescript-plugin/lib/server.ts index c3b83c3f5c..f618f8f5c8 100644 --- a/packages/typescript-plugin/lib/server.ts +++ b/packages/typescript-plugin/lib/server.ts @@ -8,10 +8,9 @@ import { getImportPathForFile } from './requests/getImportPathForFile'; import { getPropertiesAtLocation } from './requests/getPropertiesAtLocation'; import { getQuickInfoAtPosition } from './requests/getQuickInfoAtPosition'; import type { RequestContext } from './requests/types'; -import { connect, getNamedPipePath } from './utils'; +import { getServerPath } from './utils'; -export interface Request { - type: 'containsFile' +export type RequestType = 'containsFile' | 'projectInfo' | 'collectExtractProps' | 'getImportPathForFile' @@ -21,10 +20,25 @@ export interface Request { | 'getComponentProps' | 'getComponentEvents' | 'getTemplateContextProps' - | 'getComponentNames' | 'getElementAttrs'; - args: [fileName: string, ...rest: any]; -} + +export type RequestData = [ + seq: number, + type: RequestType, + fileName: string, + ...args: any[], +]; + +export type ResponseData = [ + seq: number, + data: any, +]; + +export type NotificationData = [ + type: 'componentAndPropsUpdated', + fileName: string, + data: any, +]; export interface ProjectInfo { name: string; @@ -38,85 +52,53 @@ export async function startNamedPipeServer( language: Language, projectKind: ts.server.ProjectKind.Inferred | ts.server.ProjectKind.Configured ) { + let lastProjectVersion: string | undefined; + + const requestContext: RequestContext = { + typescript: ts, + languageService: info.languageService, + languageServiceHost: info.languageServiceHost, + language: language, + isTsPlugin: true, + getFileId: (fileName: string) => fileName, + }; + const dataChunks: Buffer[] = []; + const componentNamesAndProps = new Map(); + const allConnections = new Set(); const server = net.createServer(connection => { - connection.on('data', data => { - const text = data.toString(); - if (text === 'ping') { - connection.write('pong'); - return; - } - const request: Request = JSON.parse(text); - const fileName = request.args[0]; - const requestContext: RequestContext = { - typescript: ts, - languageService: info.languageService, - languageServiceHost: info.languageServiceHost, - language: language, - isTsPlugin: true, - getFileId: (fileName: string) => fileName, - }; - if (request.type === 'containsFile') { - sendResponse( - info.project.containsFile(ts.server.toNormalizedPath(fileName)) - ); - } - else if (request.type === 'projectInfo') { - sendResponse({ - name: info.project.getProjectName(), - kind: info.project.projectKind, - currentDirectory: info.project.getCurrentDirectory(), - } satisfies ProjectInfo); - } - else if (request.type === 'collectExtractProps') { - const result = collectExtractProps.apply(requestContext, request.args as any); - sendResponse(result); - } - else if (request.type === 'getImportPathForFile') { - const result = getImportPathForFile.apply(requestContext, request.args as any); - sendResponse(result); - } - else if (request.type === 'getPropertiesAtLocation') { - const result = getPropertiesAtLocation.apply(requestContext, request.args as any); - sendResponse(result); - } - else if (request.type === 'getQuickInfoAtPosition') { - const result = getQuickInfoAtPosition.apply(requestContext, request.args as any); - sendResponse(result); - } - // Component Infos - else if (request.type === 'getComponentProps') { - const result = getComponentProps.apply(requestContext, request.args as any); - sendResponse(result); - } - else if (request.type === 'getComponentEvents') { - const result = getComponentEvents.apply(requestContext, request.args as any); - sendResponse(result); - } - else if (request.type === 'getTemplateContextProps') { - const result = getTemplateContextProps.apply(requestContext, request.args as any); - sendResponse(result); - } - else if (request.type === 'getComponentNames') { - const result = getComponentNames.apply(requestContext, request.args as any); - sendResponse(result); - } - else if (request.type === 'getElementAttrs') { - const result = getElementAttrs.apply(requestContext, request.args as any); - sendResponse(result); - } - else { - console.warn('[Vue Named Pipe Server] Unknown request type:', request.type); + allConnections.add(connection); + + connection.on('end', () => { + allConnections.delete(connection); + }); + connection.on('data', buffer => { + dataChunks.push(buffer); + const text = dataChunks.toString(); + if (text.endsWith('\n\n')) { + dataChunks.length = 0; + const requests = text.split('\n\n'); + for (let json of requests) { + json = json.trim(); + if (!json) { + continue; + } + try { + onRequest(connection, JSON.parse(json)); + } catch (e) { + console.error('[Vue Named Pipe Server] JSON parse error:', e); + } + } } }); connection.on('error', err => console.error('[Vue Named Pipe Server]', err.message)); - function sendResponse(data: any | undefined) { - connection.write(JSON.stringify(data ?? null) + '\n\n'); + for (const [fileName, data] of componentNamesAndProps) { + notify(connection, 'componentAndPropsUpdated', fileName, data); } }); - for (let i = 0; i < 20; i++) { - const path = getNamedPipePath(projectKind, i); + for (let i = 0; i < 10; i++) { + const path = getServerPath(projectKind, i); const socket = await connect(path, 100); if (typeof socket === 'object') { socket.end(); @@ -130,6 +112,167 @@ export async function startNamedPipeServer( break; } } + + updateWhile(); + + async function updateWhile() { + while (true) { + await sleep(500); + const projectVersion = info.project.getProjectVersion(); + if (lastProjectVersion === projectVersion) { + continue; + } + const connections = [...allConnections].filter(c => !c.destroyed); + if (!connections.length) { + continue; + } + const token = info.languageServiceHost.getCancellationToken?.(); + const openedScriptInfos = info.project.getRootScriptInfos().filter(info => info.isScriptOpen()); + if (!openedScriptInfos.length) { + continue; + } + for (const scriptInfo of openedScriptInfos) { + await sleep(10); + if (token?.isCancellationRequested()) { + break; + } + let newData: Record | undefined = {}; + const componentNames = getComponentNames.apply(requestContext, [scriptInfo.fileName]); + // const testProps = getComponentProps.apply(requestContext, [scriptInfo.fileName, 'HelloWorld']); + // debugger; + for (const component of componentNames ?? []) { + await sleep(10); + if (token?.isCancellationRequested()) { + newData = undefined; + break; + } + const props = getComponentProps.apply(requestContext, [scriptInfo.fileName, component]); + if (props) { + newData[component] = props; + } + } + if (!newData) { + // Canceled + break; + } + const oldDataJson = componentNamesAndProps.get(scriptInfo.fileName); + const newDataJson = JSON.stringify(newData); + if (oldDataJson !== newDataJson) { + // Update cache + componentNamesAndProps.set(scriptInfo.fileName, newDataJson); + // Notify + for (const connection of connections) { + notify(connection, 'componentAndPropsUpdated', scriptInfo.fileName, newData); + } + } + } + lastProjectVersion = projectVersion; + } + } + + function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + function notify(connection: net.Socket, type: NotificationData[0], fileName: string, data: any) { + connection.write(JSON.stringify([type, fileName, data] satisfies NotificationData) + '\n\n'); + } + + async function onRequest(connection: net.Socket, [seq, requestType, ...args]: RequestData) { + if (requestType === 'projectInfo') { + sendResponse({ + name: info.project.getProjectName(), + kind: info.project.projectKind, + currentDirectory: info.project.getCurrentDirectory(), + } satisfies ProjectInfo); + } + else if (requestType === 'containsFile') { + sendResponse( + info.project.containsFile(ts.server.toNormalizedPath(args[0])) + ); + } + else if (requestType === 'collectExtractProps') { + const result = collectExtractProps.apply(requestContext, args as any); + sendResponse(result); + } + else if (requestType === 'getImportPathForFile') { + const result = getImportPathForFile.apply(requestContext, args as any); + sendResponse(result); + } + else if (requestType === 'getPropertiesAtLocation') { + const result = getPropertiesAtLocation.apply(requestContext, args as any); + sendResponse(result); + } + else if (requestType === 'getQuickInfoAtPosition') { + const result = getQuickInfoAtPosition.apply(requestContext, args as any); + sendResponse(result); + } + else if (requestType === 'getComponentProps') { + const result = getComponentProps.apply(requestContext, args as any); + sendResponse(result); + } + else if (requestType === 'getComponentEvents') { + const result = getComponentEvents.apply(requestContext, args as any); + sendResponse(result); + } + else if (requestType === 'getTemplateContextProps') { + const result = getTemplateContextProps.apply(requestContext, args as any); + sendResponse(result); + } + else if (requestType === 'getElementAttrs') { + const result = getElementAttrs.apply(requestContext, args as any); + sendResponse(result); + } + else { + console.warn('[Vue Named Pipe Server] Unknown request:', requestType); + debugger; + } + + function sendResponse(data: any | undefined) { + connection.write(JSON.stringify([seq, data ?? null]) + '\n\n'); + } + } +} + +function connect(namedPipePath: string, timeout?: number) { + return new Promise(resolve => { + const socket = net.connect(namedPipePath); + if (timeout) { + socket.setTimeout(timeout); + } + const onConnect = () => { + cleanup(); + resolve(socket); + }; + const onError = (err: any) => { + if (err.code === 'ECONNREFUSED') { + try { + console.log('[Vue Named Pipe Client] Deleting:', namedPipePath); + fs.promises.unlink(namedPipePath); + } catch { } + } + cleanup(); + resolve('error'); + socket.end(); + }; + const onTimeout = () => { + cleanup(); + resolve('timeout'); + socket.end(); + }; + const cleanup = () => { + socket.off('connect', onConnect); + socket.off('error', onError); + socket.off('timeout', onTimeout); + }; + socket.on('connect', onConnect); + socket.on('error', onError); + socket.on('timeout', onTimeout); + }); } function tryListen(server: net.Server, namedPipePath: string) { diff --git a/packages/typescript-plugin/lib/utils.ts b/packages/typescript-plugin/lib/utils.ts index ebc318aec5..72315e49b4 100644 --- a/packages/typescript-plugin/lib/utils.ts +++ b/packages/typescript-plugin/lib/utils.ts @@ -3,211 +3,216 @@ import * as net from 'node:net'; import * as os from 'node:os'; import * as path from 'node:path'; import type * as ts from 'typescript'; -import type { ProjectInfo, Request } from './server'; +import type { NotificationData, ProjectInfo, RequestData, ResponseData } from './server'; export { TypeScriptProjectHost } from '@volar/typescript'; -const { version } = require('../package.json'); +let { version } = require('../package.json'); +if (version === '2.1.10') { + version += '-dev'; +} const platform = os.platform(); const pipeDir = platform === 'win32' - ? `\\\\.\\pipe` - : `/tmp`; -const toFullPath = (file: string) => { - if (platform === 'win32') { - return pipeDir + '\\' + file; + ? `\\\\.\\pipe\\` + : `/tmp/`; + +export function getServerPath(kind: ts.server.ProjectKind, id: number) { + if (kind === 1 satisfies ts.server.ProjectKind.Configured) { + return `${pipeDir}vue-named-pipe-${version}-configured-${id}`; + } else { + return `${pipeDir}vue-named-pipe-${version}-inferred-${id}`; } - else { - return pipeDir + '/' + file; +} + +class NamedPipeServer { + path: string; + connecting = false; + projectInfo?: ProjectInfo; + containsFileCache = new Map>(); + componentNamesAndProps = new Map>(); + + constructor(kind: ts.server.ProjectKind, id: number) { + this.path = getServerPath(kind, id); } -}; -const configuredNamedPipePathPrefix = toFullPath(`vue-named-pipe-${version}-configured-`); -const inferredNamedPipePathPrefix = toFullPath(`vue-named-pipe-${version}-inferred-`); -const pipes = new Map(); -export const onSomePipeReadyCallbacks: (() => void)[] = []; + containsFile(fileName: string) { + if (this.projectInfo) { + if (!this.containsFileCache.has(fileName)) { + this.containsFileCache.set(fileName, (async () => { + const res = await this.request('containsFile', fileName); + if (typeof res !== 'boolean') { + // If the request fails, delete the cache + this.containsFileCache.delete(fileName); + } + return res; + })()); + } + return this.containsFileCache.get(fileName); + } + } -function waitingForNamedPipeServerReady(namedPipePath: string) { - const socket = net.connect(namedPipePath); - const start = Date.now(); - socket.on('connect', () => { - console.log('[Vue Named Pipe Client] Connected:', namedPipePath, 'in', (Date.now() - start) + 'ms'); - socket.write('ping'); - }); - socket.on('data', () => { - console.log('[Vue Named Pipe Client] Ready:', namedPipePath, 'in', (Date.now() - start) + 'ms'); - pipes.set(namedPipePath, 'ready'); - socket.end(); - onSomePipeReadyCallbacks.forEach(cb => cb()); - }); - socket.on('error', err => { - if ((err as any).code === 'ECONNREFUSED') { - try { - console.log('[Vue Named Pipe Client] Deleting:', namedPipePath); - fs.promises.unlink(namedPipePath); - } catch { } + update() { + if (!this.connecting && !this.projectInfo) { + this.connecting = true; + this.connect(); } - pipes.delete(namedPipePath); - socket.end(); - }); - socket.on('timeout', () => { - pipes.delete(namedPipePath); - socket.end(); - }); -} + } -export function getNamedPipePath(projectKind: ts.server.ProjectKind.Configured | ts.server.ProjectKind.Inferred, key: number) { - return projectKind === 1 satisfies ts.server.ProjectKind.Configured - ? `${configuredNamedPipePathPrefix}${key}` - : `${inferredNamedPipePathPrefix}${key}`; -} + connect() { + this.socket = net.connect(this.path); + this.socket.on('data', this.onData.bind(this)); + this.socket.on('connect', async () => { + const projectInfo = await this.request('projectInfo', ''); + if (projectInfo) { + console.log('TSServer project ready:', projectInfo.name); + this.projectInfo = projectInfo; + this.containsFileCache.clear(); + onServerReady.forEach(cb => cb()); + } else { + this.close(); + } + }); + this.socket.on('error', err => { + if ((err as any).code === 'ECONNREFUSED') { + try { + console.log('Deleteing invalid named pipe file:', this.path); + fs.promises.unlink(this.path); + } catch { } + } + this.close(); + }); + this.socket.on('timeout', () => { + this.close(); + }); + } -export function getReadyNamedPipePaths() { - const configuredPipes: string[] = []; - const inferredPipes: string[] = []; - for (let i = 0; i < 20; i++) { - const configuredPipe = getNamedPipePath(1 satisfies ts.server.ProjectKind.Configured, i); - const inferredPipe = getNamedPipePath(0 satisfies ts.server.ProjectKind.Inferred, i); - if (pipes.get(configuredPipe) === 'ready') { - configuredPipes.push(configuredPipe); - } - else if (!pipes.has(configuredPipe)) { - pipes.set(configuredPipe, 'unknown'); - waitingForNamedPipeServerReady(configuredPipe); - } - if (pipes.get(inferredPipe) === 'ready') { - inferredPipes.push(inferredPipe); + close() { + this.connecting = false; + this.projectInfo = undefined; + this.socket?.end(); + } + + socket?: net.Socket; + seq = 0; + dataChunks: Buffer[] = []; + requestHandlers: Map void> = new Map(); + + onData(chunk: Buffer) { + this.dataChunks.push(chunk); + const data = Buffer.concat(this.dataChunks); + const text = data.toString(); + if (text.endsWith('\n\n')) { + this.dataChunks.length = 0; + const results = text.split('\n\n'); + for (let result of results) { + result = result.trim(); + if (!result) { + continue; + } + try { + const data: ResponseData | NotificationData = JSON.parse(result.trim()); + if (typeof data[0] === 'number') { + const [seq, res] = data; + this.requestHandlers.get(seq)?.(res); + } else { + const [type, fileName, res] = data; + this.onNotification(type, fileName, res); + } + } catch (e) { + console.error('JSON parse error:', e); + } + } } - else if (!pipes.has(inferredPipe)) { - pipes.set(inferredPipe, 'unknown'); - waitingForNamedPipeServerReady(inferredPipe); + } + + onNotification(type: NotificationData[0], fileName: string, data: any) { + // console.log(`[${type}] ${fileName} ${JSON.stringify(data)}`); + if (type === 'componentAndPropsUpdated') { + this.componentNamesAndProps.set(fileName, data); } } - return { - configured: configuredPipes, - inferred: inferredPipes, - }; + + request(requestType: RequestData[1], fileName: string, ...args: any[]) { + return new Promise(resolve => { + const seq = this.seq++; + // console.time(`[${seq}] ${requestType} ${fileName}`); + this.requestHandlers.set(seq, data => { + // console.timeEnd(`[${seq}] ${requestType} ${fileName}`); + this.requestHandlers.delete(seq); + resolve(data); + }); + this.socket!.write(JSON.stringify([seq, requestType, fileName, ...args] satisfies RequestData) + '\n\n'); + }); + } } -export function connect(namedPipePath: string, timeout?: number) { - return new Promise(resolve => { - const socket = net.connect(namedPipePath); - if (timeout) { - socket.setTimeout(timeout); - } - const onConnect = () => { - cleanup(); - resolve(socket); - }; - const onError = (err: any) => { - if (err.code === 'ECONNREFUSED') { - try { - console.log('[Vue Named Pipe Client] Deleting:', namedPipePath); - fs.promises.unlink(namedPipePath); - } catch { } - } - pipes.delete(namedPipePath); - cleanup(); - resolve('error'); - socket.end(); - }; - const onTimeout = () => { - cleanup(); - resolve('timeout'); - socket.end(); - }; - const cleanup = () => { - socket.off('connect', onConnect); - socket.off('error', onError); - socket.off('timeout', onTimeout); - }; - socket.on('connect', onConnect); - socket.on('error', onError); - socket.on('timeout', onTimeout); - }); +export const configuredServers: NamedPipeServer[] = []; +export const inferredServers: NamedPipeServer[] = []; +export const onServerReady: (() => void)[] = []; + +for (let i = 0; i < 10; i++) { + configuredServers.push(new NamedPipeServer(1 satisfies ts.server.ProjectKind.Configured, i)); + inferredServers.push(new NamedPipeServer(0 satisfies ts.server.ProjectKind.Inferred, i)); } -export async function searchNamedPipeServerForFile(fileName: string) { - const paths = await getReadyNamedPipePaths(); +export async function getBestServer(fileName: string) { + for (const server of configuredServers) { + server.update(); + } - const configuredServers = (await Promise.all( - paths.configured.map(async path => { - // Find existing servers - const socket = await connect(path); - if (typeof socket !== 'object') { + let servers = (await Promise.all( + configuredServers.map(async server => { + const projectInfo = server.projectInfo; + if (!projectInfo) { return; } - - // Find servers containing the current file - const containsFile = await sendRequestWorker({ type: 'containsFile', args: [fileName] }, socket); + const containsFile = await server.containsFile(fileName); if (!containsFile) { - socket.end(); - return; - } - - // Get project info for each server - const projectInfo = await sendRequestWorker({ type: 'projectInfo', args: [fileName] }, socket); - if (!projectInfo) { - socket.end(); return; } - - return { - socket, - projectInfo, - }; + return server; }) )).filter(server => !!server); // Sort servers by tsconfig - configuredServers.sort((a, b) => sortTSConfigs(fileName, a.projectInfo.name, b.projectInfo.name)); + servers.sort((a, b) => sortTSConfigs(fileName, a.projectInfo!.name, b.projectInfo!.name)); - if (configuredServers.length) { - // Close all but the first server - for (let i = 1; i < configuredServers.length; i++) { - configuredServers[i].socket.end(); - } + if (servers.length) { // Return the first server - return configuredServers[0]; + return servers[0]; } - const inferredServers = (await Promise.all( - paths.inferred.map(async namedPipePath => { - // Find existing servers - const socket = await connect(namedPipePath); - if (typeof socket !== 'object') { - return; - } + for (const server of inferredServers) { + server.update(); + } - // Get project info for each server - const projectInfo = await sendRequestWorker({ type: 'projectInfo', args: [fileName] }, socket); + servers = (await Promise.all( + inferredServers.map(server => { + const projectInfo = server.projectInfo; if (!projectInfo) { - socket.end(); return; } - // Check if the file is in the project's directory - if (!path.relative(projectInfo.currentDirectory, fileName).startsWith('..')) { - return { - socket, - projectInfo, - }; + if (path.relative(projectInfo.currentDirectory, fileName).startsWith('..')) { + return; } + return server; }) )).filter(server => !!server); // Sort servers by directory - inferredServers.sort((a, b) => - b.projectInfo.currentDirectory.replace(/\\/g, '/').split('/').length - - a.projectInfo.currentDirectory.replace(/\\/g, '/').split('/').length + servers.sort((a, b) => + b.projectInfo!.currentDirectory.replace(/\\/g, '/').split('/').length + - a.projectInfo!.currentDirectory.replace(/\\/g, '/').split('/').length ); - if (inferredServers.length) { - // Close all but the first server - for (let i = 1; i < inferredServers.length; i++) { - inferredServers[i].socket.end(); - } + if (servers.length) { // Return the first server - return inferredServers[0]; + return servers[0]; } } @@ -232,36 +237,3 @@ function isFileInDir(fileName: string, dir: string) { const relative = path.relative(dir, fileName); return !!relative && !relative.startsWith('..') && !path.isAbsolute(relative); } - -export function sendRequestWorker(request: Request, socket: net.Socket) { - return new Promise(resolve => { - let dataChunks: Buffer[] = []; - const onData = (chunk: Buffer) => { - dataChunks.push(chunk); - const data = Buffer.concat(dataChunks); - const text = data.toString(); - if (text.endsWith('\n\n')) { - let json = null; - try { - json = JSON.parse(text); - } catch (e) { - console.error('[Vue Named Pipe Client] Failed to parse response:', text); - } - cleanup(); - resolve(json); - } - }; - const onError = (err: any) => { - console.error('[Vue Named Pipe Client] Error:', err.message); - cleanup(); - resolve(undefined); - }; - const cleanup = () => { - socket.off('data', onData); - socket.off('error', onError); - }; - socket.on('data', onData); - socket.on('error', onError); - socket.write(JSON.stringify(request)); - }); -}