From b16d5562e681584a14895e713521db7c2f1b0144 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 20 Mar 2024 04:43:56 +0800 Subject: [PATCH] wip [skip ci] --- extensions/vscode/src/features/doctor.ts | 65 ++++----- packages/language-server/node.ts | 15 +- packages/language-service/index.ts | 108 ++++++++++---- .../lib/plugins/vue-autoinsert-dotvalue.ts | 5 +- .../lib/plugins/vue-extract-file.ts | 5 +- .../lib/plugins/vue-template.ts | 57 ++++++-- .../lib/plugins/vue-twoslash-queries.ts | 5 +- .../tests/utils/createTester.ts | 5 +- packages/typescript-plugin/index.ts | 9 +- packages/typescript-plugin/lib/client.ts | 64 +-------- packages/typescript-plugin/lib/common.ts | 133 +++++++++++------- .../lib/requests/collectExtractProps.ts | 22 +-- .../lib/requests/componentInfos.ts | 102 ++++++++------ .../lib/requests/containsFile.ts | 5 - .../lib/requests/getPropertiesAtLocation.ts | 23 +-- .../lib/requests/getQuickInfoAtPosition.ts | 21 ++- packages/typescript-plugin/lib/server.ts | 113 +++++++++------ packages/typescript-plugin/lib/utils.ts | 75 +++++++--- 18 files changed, 482 insertions(+), 350 deletions(-) delete mode 100644 packages/typescript-plugin/lib/requests/containsFile.ts diff --git a/extensions/vscode/src/features/doctor.ts b/extensions/vscode/src/features/doctor.ts index 9008942fc..1cab87c45 100644 --- a/extensions/vscode/src/features/doctor.ts +++ b/extensions/vscode/src/features/doctor.ts @@ -134,30 +134,6 @@ export async function register(context: vscode.ExtensionContext, client: BaseLan }); } - // check should use @volar-plugins/vetur instead of vetur - const vetur = vscode.extensions.getExtension('octref.vetur'); - if (vetur?.isActive) { - problems.push({ - title: 'Use volar-service-vetur instead of Vetur', - message: 'Detected Vetur enabled. Consider disabling Vetur and use [volar-service-vetur](https://github.com/volarjs/services/tree/master/packages/vetur) instead.', - }); - } - - // #3942, https://github.com/microsoft/TypeScript/issues/57633 - for (const extId of ['svelte.svelte-vscode', 'styled-components.vscode-styled-components']) { - const ext = vscode.extensions.getExtension(extId); - if (ext) { - problems.push({ - title: `Recommended to disable "${ext.packageJSON.displayName || extId}" in Vue workspace`, - message: [ - `This extension's TypeScript Plugin and Vue's TypeScript Plugin are known to cause some conflicts. Until the problem is resolved, it is recommended that you temporarily disable the this extension in the Vue workspace.`, - '', - 'Issues: https://github.com/vuejs/language-tools/issues/3942, https://github.com/microsoft/TypeScript/issues/57633', - ].join('\n'), - }); - } - } - // check using pug but don't install @vue/language-plugin-pug if ( sfc?.descriptor.template?.lang === 'pug' @@ -236,18 +212,35 @@ export async function register(context: vscode.ExtensionContext, client: BaseLan }); } - // #3942 - const namedPipe = await client.sendRequest(GetConnectedNamedPipeServerRequest.type, fileUri.fsPath.replace(/\\/g, '/')); - if (namedPipe?.serverKind === 0) { - problems.push({ - title: 'Missing jsconfig/tsconfig', - message: [ - 'The current file does not have a matching tsconfig/jsconfig, and extension version 2.0 will not work properly for this at the moment.', - 'To avoid this problem, you can create a jsconfig in the project root, or downgrade to 1.8.27.', - '', - 'Issue: https://github.com/vuejs/language-tools/issues/3942', - ].join('\n'), - }); + if (config.server.hybridMode) { + // #3942 + const namedPipe = await client.sendRequest(GetConnectedNamedPipeServerRequest.type, fileUri.fsPath.replace(/\\/g, '/')); + if (namedPipe?.serverKind === 0) { + problems.push({ + title: 'Missing jsconfig/tsconfig', + message: [ + 'The current file does not have a matching tsconfig/jsconfig, and extension version 2.0 will not work properly for this at the moment.', + 'To avoid this problem, you can create a jsconfig in the project root, or downgrade to 1.8.27.', + '', + 'Issue: https://github.com/vuejs/language-tools/issues/3942', + ].join('\n'), + }); + } + + // #3942, https://github.com/microsoft/TypeScript/issues/57633 + for (const extId of ['svelte.svelte-vscode', 'styled-components.vscode-styled-components']) { + const ext = vscode.extensions.getExtension(extId); + if (ext) { + problems.push({ + title: `Recommended to disable "${ext.packageJSON.displayName || extId}" in Vue workspace`, + message: [ + `This extension's TypeScript Plugin and Vue's TypeScript Plugin are known to cause some conflicts. Until the problem is resolved, it is recommended that you temporarily disable the this extension in the Vue workspace.`, + '', + 'Issues: https://github.com/vuejs/language-tools/issues/3942, https://github.com/microsoft/TypeScript/issues/57633', + ].join('\n'), + }); + } + } } // check outdated vue language plugins diff --git a/packages/language-server/node.ts b/packages/language-server/node.ts index e93fb92a8..6035e5316 100644 --- a/packages/language-server/node.ts +++ b/packages/language-server/node.ts @@ -5,6 +5,7 @@ import { ServiceEnvironment, convertAttrName, convertTagName, createVueServicePl import { DetectNameCasingRequest, GetConvertAttrCasingEditsRequest, GetConvertTagCasingEditsRequest, ParseSFCRequest } from './lib/protocol'; import type { VueInitializationOptions } from './lib/types'; import * as tsPluginClient from '@vue/typescript-plugin/lib/client'; +import { searchNamedPipeServerForFile } from '@vue/typescript-plugin/lib/utils'; import { GetConnectedNamedPipeServerRequest } from './lib/protocol'; export const connection: Connection = createConnection(); @@ -39,7 +40,11 @@ connection.onInitialize(async params => { { watchFileExtensions: ['js', 'cjs', 'mjs', 'ts', 'cts', 'mts', 'jsx', 'tsx', 'json', ...vueFileExtensions], getServicePlugins() { - return createVueServicePlugins(tsdk.typescript, env => envToVueOptions.get(env)!, options.vue.hybridMode, tsPluginClient); + return createVueServicePlugins( + tsdk.typescript, + env => envToVueOptions.get(env)!, + options.vue.hybridMode ? () => tsPluginClient : undefined, + ); }, async getLanguagePlugins(serviceEnv, projectContext) { const commandLine = await parseCommandLine(); @@ -101,8 +106,10 @@ connection.onInitialize(async params => { }, ); - // handle by tsserver + @vue/typescript-plugin - result.capabilities.semanticTokensProvider = undefined; + if (options.vue.hybridMode) { + // handle by tsserver + @vue/typescript-plugin + result.capabilities.semanticTokensProvider = undefined; + } return result; }); @@ -141,7 +148,7 @@ connection.onRequest(GetConvertAttrCasingEditsRequest.type, async params => { }); connection.onRequest(GetConnectedNamedPipeServerRequest.type, async fileName => { - const server = await tsPluginClient.searchNamedPipeServerForFile(fileName); + const server = await searchNamedPipeServerForFile(fileName); if (server) { return server; } diff --git a/packages/language-service/index.ts b/packages/language-service/index.ts index 5e90ad346..ef4a049fd 100644 --- a/packages/language-service/index.ts +++ b/packages/language-service/index.ts @@ -3,14 +3,13 @@ export * from '@vue/language-core'; export * from './lib/ideFeatures/nameCasing'; export * from './lib/types'; -import type { ServiceEnvironment, ServicePlugin } from '@volar/language-service'; +import type { ServiceContext, ServiceEnvironment, ServicePlugin } from '@volar/language-service'; import type { VueCompilerOptions } from './lib/types'; -import { decorateLanguageServiceForVue } from '@vue/typescript-plugin/lib/common'; import { create as createEmmetServicePlugin } from 'volar-service-emmet'; import { create as createJsonServicePlugin } from 'volar-service-json'; import { create as createPugFormatServicePlugin } from 'volar-service-pug-beautify'; -import { create as createTypeScriptServicePlugin } from 'volar-service-typescript'; +import { create as createTypeScriptServicePlugins } from 'volar-service-typescript'; import { create as createTypeScriptTwoslashQueriesServicePlugin } from 'volar-service-typescript-twoslash-queries'; import { create as createTypeScriptDocCommentTemplateServicePlugin } from 'volar-service-typescript/lib/plugins/docCommentTemplate'; import { create as createTypeScriptSyntacticServicePlugin } from 'volar-service-typescript/lib/plugins/syntactic'; @@ -28,34 +27,21 @@ import { create as createVueToggleVBindServicePlugin } from './lib/plugins/vue-t import { create as createVueTwoslashQueriesServicePlugin } from './lib/plugins/vue-twoslash-queries'; import { create as createVueVisualizeHiddenCallbackParamServicePlugin } from './lib/plugins/vue-visualize-hidden-callback-param'; +import { decorateLanguageServiceForVue } from '@vue/typescript-plugin/lib/common'; +import { collectExtractProps } from '@vue/typescript-plugin/lib/requests/collectExtractProps'; +import { getComponentEvents, getComponentNames, getComponentProps, getElementAttrs, getTemplateContextProps } from '@vue/typescript-plugin/lib/requests/componentInfos'; +import { getPropertiesAtLocation } from '@vue/typescript-plugin/lib/requests/getPropertiesAtLocation'; +import { getQuickInfoAtPosition } from '@vue/typescript-plugin/lib/requests/getQuickInfoAtPosition'; + export function createVueServicePlugins( ts: typeof import('typescript'), getVueOptions: (env: ServiceEnvironment) => VueCompilerOptions, - hybridMode = true, - tsPluginClient?: typeof import('@vue/typescript-plugin/lib/client'), + getTsPluginClient?: (context: ServiceContext) => typeof import('@vue/typescript-plugin/lib/client') | undefined, ): ServicePlugin[] { - const plugins: ServicePlugin[] = [ - createTypeScriptTwoslashQueriesServicePlugin(ts), - createCssServicePlugin(), - createPugFormatServicePlugin(), - createJsonServicePlugin(), - createVueTemplateServicePlugin('html', ts, getVueOptions, tsPluginClient), - createVueTemplateServicePlugin('pug', ts, getVueOptions, tsPluginClient), - createVueSfcServicePlugin(), - createVueTwoslashQueriesServicePlugin(ts, tsPluginClient), - createVueReferencesCodeLensServicePlugin(), - createVueDocumentDropServicePlugin(ts), - createVueAutoDotValueServicePlugin(ts, tsPluginClient), - createVueAutoWrapParenthesesServicePlugin(ts), - createVueAutoAddSpaceServicePlugin(), - createVueVisualizeHiddenCallbackParamServicePlugin(), - createVueDirectiveCommentsServicePlugin(), - createVueExtractFileServicePlugin(ts, tsPluginClient), - createVueToggleVBindServicePlugin(ts), - createEmmetServicePlugin(), - ]; + const plugins: ServicePlugin[] = []; + const hybridMode = !!getTsPluginClient; if (!hybridMode) { - plugins.push(...createTypeScriptServicePlugin(ts)); + plugins.push(...createTypeScriptServicePlugins(ts)); for (let i = 0; i < plugins.length; i++) { const plugin = plugins[i]; if (plugin.name === 'typescript-semantic') { @@ -63,18 +49,82 @@ export function createVueServicePlugins( ...plugin, create(context) { const created = plugin.create(context); + if (!context.language.typescript) { + return created; + } const languageService = (created.provide as import('volar-service-typescript').Provide)['typescript/languageService'](); const vueOptions = getVueOptions(context.env); - decorateLanguageServiceForVue(context.language.files, languageService, vueOptions, ts); + decorateLanguageServiceForVue(context.language.files, languageService, vueOptions, ts, false); return created; }, }; + break; } } + getTsPluginClient = context => { + if (!context.language.typescript) { + return; + } + const requestContext = { + typescript: ts, + files: context.language.files, + languageService: context.inject<(import('volar-service-typescript').Provide), 'typescript/languageService'>('typescript/languageService'), + vueOptions: getVueOptions(context.env), + isTsPlugin: false, + }; + return { + async collectExtractProps(...args) { + return await collectExtractProps.apply(requestContext, args); + }, + async getPropertiesAtLocation(...args) { + return await getPropertiesAtLocation.apply(requestContext, args); + }, + async getComponentEvents(...args) { + return await getComponentEvents.apply(requestContext, args); + }, + async getComponentNames(...args) { + return await getComponentNames.apply(requestContext, args); + }, + async getComponentProps(...args) { + return await getComponentProps.apply(requestContext, args); + }, + async getElementAttrs(...args) { + return await getElementAttrs.apply(requestContext, args); + }, + async getTemplateContextProps(...args) { + return await getTemplateContextProps.apply(requestContext, args); + }, + async getQuickInfoAtPosition(...args) { + return await getQuickInfoAtPosition.apply(requestContext, args); + }, + }; + }; } else { - plugins.push(createTypeScriptSyntacticServicePlugin(ts)); - plugins.push(createTypeScriptDocCommentTemplateServicePlugin(ts)); + plugins.push( + createTypeScriptSyntacticServicePlugin(ts), + createTypeScriptDocCommentTemplateServicePlugin(ts), + ); } + plugins.push( + createTypeScriptTwoslashQueriesServicePlugin(ts), + createCssServicePlugin(), + createPugFormatServicePlugin(), + createJsonServicePlugin(), + createVueTemplateServicePlugin('html', ts, getVueOptions, getTsPluginClient), + createVueTemplateServicePlugin('pug', ts, getVueOptions, getTsPluginClient), + createVueSfcServicePlugin(), + createVueTwoslashQueriesServicePlugin(ts, getTsPluginClient), + createVueReferencesCodeLensServicePlugin(), + createVueDocumentDropServicePlugin(ts), + createVueAutoDotValueServicePlugin(ts, getTsPluginClient), + createVueAutoWrapParenthesesServicePlugin(ts), + createVueAutoAddSpaceServicePlugin(), + createVueVisualizeHiddenCallbackParamServicePlugin(), + createVueDirectiveCommentsServicePlugin(), + createVueExtractFileServicePlugin(ts, getTsPluginClient), + createVueToggleVBindServicePlugin(ts), + createEmmetServicePlugin(), + ); return plugins; } diff --git a/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts b/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts index 3bf3a917c..c74838885 100644 --- a/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts +++ b/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts @@ -1,4 +1,4 @@ -import type { ServicePlugin, ServicePluginInstance } from '@volar/language-service'; +import type { ServiceContext, ServicePlugin, ServicePluginInstance } from '@volar/language-service'; import { hyphenateAttr } from '@vue/language-core'; import type * as ts from 'typescript'; import type * as vscode from 'vscode-languageserver-protocol'; @@ -17,11 +17,12 @@ function getAst(ts: typeof import('typescript'), fileName: string, snapshot: ts. export function create( ts: typeof import('typescript'), - tsPluginClient?: typeof import('@vue/typescript-plugin/lib/client'), + getTsPluginClient?: (context: ServiceContext) => typeof import('@vue/typescript-plugin/lib/client') | undefined, ): ServicePlugin { return { name: 'vue-autoinsert-dotvalue', create(context): ServicePluginInstance { + const tsPluginClient = getTsPluginClient?.(context); let currentReq = 0; return { async provideAutoInsertionEdit(document, position, lastChange) { diff --git a/packages/language-service/lib/plugins/vue-extract-file.ts b/packages/language-service/lib/plugins/vue-extract-file.ts index ff1e2abf2..20fa13147 100644 --- a/packages/language-service/lib/plugins/vue-extract-file.ts +++ b/packages/language-service/lib/plugins/vue-extract-file.ts @@ -1,4 +1,4 @@ -import type { CreateFile, ServicePlugin, TextDocumentEdit, TextEdit } from '@volar/language-service'; +import type { CreateFile, ServiceContext, ServicePlugin, TextDocumentEdit, TextEdit } from '@volar/language-service'; import type { ExpressionNode, TemplateChildNode } from '@vue/compiler-dom'; import { Sfc, VueGeneratedCode, scriptRanges } from '@vue/language-core'; import type * as ts from 'typescript'; @@ -14,11 +14,12 @@ const unicodeReg = /\\u/g; export function create( ts: typeof import('typescript'), - tsPluginClient?: typeof import('@vue/typescript-plugin/lib/client'), + getTsPluginClient?: (context: ServiceContext) => typeof import('@vue/typescript-plugin/lib/client') | undefined, ): ServicePlugin { return { name: 'vue-extract-file', create(context) { + const tsPluginClient = getTsPluginClient?.(context); return { async provideCodeActions(document, range, _context) { diff --git a/packages/language-service/lib/plugins/vue-template.ts b/packages/language-service/lib/plugins/vue-template.ts index 1f206f00e..58cd5673f 100644 --- a/packages/language-service/lib/plugins/vue-template.ts +++ b/packages/language-service/lib/plugins/vue-template.ts @@ -1,4 +1,4 @@ -import type { Disposable, ServiceEnvironment, ServicePluginInstance } from '@volar/language-service'; +import type { Disposable, ServiceContext, ServiceEnvironment, ServicePluginInstance } from '@volar/language-service'; import { VueGeneratedCode, hyphenateAttr, hyphenateTag, parseScriptSetupRanges, tsCodegen } from '@vue/language-core'; import { camelize, capitalize } from '@vue/shared'; import { create as createHtmlService } from 'volar-service-html'; @@ -10,6 +10,7 @@ import { getNameCasing } from '../ideFeatures/nameCasing'; import { AttrNameCasing, ServicePlugin, TagNameCasing, VueCompilerOptions } from '../types'; import { loadModelModifiersData, loadTemplateData } from './data'; import { URI, Utils } from 'vscode-uri'; +import { getComponentSpans } from '@vue/typescript-plugin/lib/common'; let builtInData: html.HTMLDataV1; let modelData: html.HTMLDataV1; @@ -18,7 +19,7 @@ export function create( mode: 'html' | 'pug', ts: typeof import('typescript'), getVueOptions: (env: ServiceEnvironment) => VueCompilerOptions, - tsPluginClient?: typeof import('@vue/typescript-plugin/lib/client'), + getTsPluginClient?: (context: ServiceContext) => typeof import('@vue/typescript-plugin/lib/client') | undefined, ): ServicePlugin { let customData: html.IHTMLDataProvider[] = []; @@ -51,7 +52,7 @@ export function create( '@', // vue event shorthand ], create(context): ServicePluginInstance { - + const tsPluginClient = getTsPluginClient?.(context); const baseServiceInstance = baseService.create(context); const vueCompilerOptions = getVueOptions(context.env); @@ -327,6 +328,45 @@ export function create( ]; } }, + + provideDocumentSemanticTokens(document, range, legend) { + if (!isSupportedDocument(document)) { + return; + } + const [_virtualCode, sourceFile] = context.documents.getVirtualCodeByUri(document.uri); + if ( + !sourceFile + || !(sourceFile.generated?.code instanceof VueGeneratedCode) + || !sourceFile.generated.code.sfc.template + ) { + return []; + } + const { template } = sourceFile.generated.code.sfc; + const spans = getComponentSpans.call( + { + files: context.language.files, + languageService: context.inject<(import('volar-service-typescript').Provide), 'typescript/languageService'>('typescript/languageService'), + typescript: ts, + vueOptions: getVueOptions(context.env), + }, + sourceFile.generated.code, + template, + { + start: document.offsetAt(range.start), + length: document.offsetAt(range.end) - document.offsetAt(range.start), + }); + const classTokenIndex = legend.tokenTypes.indexOf('class'); + return spans.map(span => { + const start = document.positionAt(span.start); + return [ + start.line, + start.character, + span.length, + classTokenIndex, + 0, + ]; + }); + }, }; async function provideHtmlData(sourceDocumentUri: string, vueCode: VueGeneratedCode) { @@ -419,12 +459,8 @@ export function create( return tags; }, provideAttributes: tag => { + const tagInfo = tagInfos.get(tag); - - - let failed = false; - - let tagInfo = tagInfos.get(tag); if (!tagInfo) { promises.push((async () => { const attrs = await tsPluginClient?.getElementAttrs(vueCode.fileName, tag) ?? []; @@ -440,11 +476,6 @@ export function create( return []; } - - if (failed) { - return []; - } - const { attrs, props, events } = tagInfo; const attributes: html.IAttributeData[] = []; const _tsCodegen = tsCodegen.get(vueCode.sfc); diff --git a/packages/language-service/lib/plugins/vue-twoslash-queries.ts b/packages/language-service/lib/plugins/vue-twoslash-queries.ts index 505946378..6c74e2c20 100644 --- a/packages/language-service/lib/plugins/vue-twoslash-queries.ts +++ b/packages/language-service/lib/plugins/vue-twoslash-queries.ts @@ -1,4 +1,4 @@ -import type { ServicePlugin, ServicePluginInstance } from '@volar/language-service'; +import type { ServiceContext, ServicePlugin, ServicePluginInstance } from '@volar/language-service'; import * as vue from '@vue/language-core'; import type * as vscode from 'vscode-languageserver-protocol'; @@ -6,11 +6,12 @@ const twoslashReg = //g; export function create( ts: typeof import('typescript'), - tsPluginClient?: typeof import('@vue/typescript-plugin/lib/client'), + getTsPluginClient?: (context: ServiceContext) => typeof import('@vue/typescript-plugin/lib/client') | undefined, ): ServicePlugin { return { name: 'vue-twoslash-queries', create(context): ServicePluginInstance { + const tsPluginClient = getTsPluginClient?.(context); return { async provideInlayHints(document, range) { diff --git a/packages/language-service/tests/utils/createTester.ts b/packages/language-service/tests/utils/createTester.ts index 4ea9ac639..a0b7acb95 100644 --- a/packages/language-service/tests/utils/createTester.ts +++ b/packages/language-service/tests/utils/createTester.ts @@ -1,7 +1,7 @@ import { TypeScriptProjectHost, createLanguageService, resolveCommonLanguageId } from '@volar/language-service'; import { createLanguage } from '@volar/typescript'; import * as path from 'path'; -import type * as ts from 'typescript'; +import * as ts from 'typescript'; import { URI } from 'vscode-uri'; import { createParsedCommandLine, createVueLanguagePlugin, createVueServicePlugins } from '../..'; import { createMockServiceEnv } from './mockEnv'; @@ -11,7 +11,6 @@ export const tester = createTester(rootUri); function createTester(rootUri: string) { - const ts = require('typescript') as typeof import('typescript'); const serviceEnv = createMockServiceEnv(rootUri, () => currentVSCodeSettings ?? defaultVSCodeSettings); const rootPath = serviceEnv.typescript!.uriToFileName(rootUri.toString()); const realTsConfig = path.join(rootPath, 'tsconfig.json').replace(/\\/g, '/'); @@ -46,7 +45,7 @@ function createTester(rootUri: string) { parsedCommandLine.options, parsedCommandLine.vueOptions, ); - const vueServicePlugins = createVueServicePlugins(ts, () => parsedCommandLine.vueOptions, false); + const vueServicePlugins = createVueServicePlugins(ts, () => parsedCommandLine.vueOptions); const defaultVSCodeSettings: any = { 'typescript.preferences.quoteStyle': 'single', 'javascript.preferences.quoteStyle': 'single', diff --git a/packages/typescript-plugin/index.ts b/packages/typescript-plugin/index.ts index ed4c3ec0f..727b0cd7f 100644 --- a/packages/typescript-plugin/index.ts +++ b/packages/typescript-plugin/index.ts @@ -4,8 +4,7 @@ import * as vue from '@vue/language-core'; import { createFileRegistry, resolveCommonLanguageId } from '@vue/language-core'; import type * as ts from 'typescript'; import { decorateLanguageServiceForVue } from './lib/common'; -import { startNamedPipeServer } from './lib/server'; -import { projects } from './lib/utils'; +import { startNamedPipeServer, projects } from './lib/server'; const windowsPathReg = /\\/g; const externalFiles = new WeakMap>(); @@ -69,12 +68,12 @@ function createLanguageServicePlugin(): ts.server.PluginModuleFactory { ); projectExternalFileExtensions.set(info.project, extensions); - projects.set(info.project, { info, files, ts, vueOptions }); + projects.set(info.project, { info, files, vueOptions }); decorateLanguageService(files, info.languageService); - decorateLanguageServiceForVue(files, info.languageService, vueOptions, ts); + decorateLanguageServiceForVue(files, info.languageService, vueOptions, ts, true); decorateLanguageServiceHost(files, info.languageServiceHost, ts); - startNamedPipeServer(info.project.projectKind, info.project.getCurrentDirectory()); + startNamedPipeServer(ts, info.project.projectKind, info.project.getCurrentDirectory()); } return info.languageService; diff --git a/packages/typescript-plugin/lib/client.ts b/packages/typescript-plugin/lib/client.ts index 4090d0cd9..884ec1a4e 100644 --- a/packages/typescript-plugin/lib/client.ts +++ b/packages/typescript-plugin/lib/client.ts @@ -1,10 +1,5 @@ -import * as fs from 'fs'; -import type * as net from 'net'; -import * as path from 'path'; -import type * as ts from 'typescript'; import type { Request } from './server'; -import type { NamedPipeServer } from './utils'; -import { connect, pipeTable } from './utils'; +import { connect, searchNamedPipeServerForFile, sendRequestWorker } from './utils'; export function collectExtractProps( ...args: Parameters @@ -93,60 +88,3 @@ async function sendRequest(request: Request) { } return await sendRequestWorker(request, client); } - -export async function searchNamedPipeServerForFile(fileName: string) { - if (!fs.existsSync(pipeTable)) { - return; - } - const servers: NamedPipeServer[] = JSON.parse(fs.readFileSync(pipeTable, 'utf8')); - const configuredServers = servers - .filter(item => item.serverKind === 1 satisfies ts.server.ProjectKind.Configured); - const inferredServers = servers - .filter(item => item.serverKind === 0 satisfies ts.server.ProjectKind.Inferred) - .sort((a, b) => b.currentDirectory.length - a.currentDirectory.length); - for (const server of configuredServers) { - const client = await connect(server.path); - if (client) { - const response = await sendRequestWorker({ type: 'containsFile', args: [fileName] }, client); - if (response) { - return server; - } - } - } - for (const server of inferredServers) { - if (!path.relative(server.currentDirectory, fileName).startsWith('..')) { - const client = await connect(server.path); - if (client) { - return server; - } - } - } -} - -function sendRequestWorker(request: Request, client: net.Socket) { - return new Promise(resolve => { - let dataChunks: Buffer[] = []; - client.on('data', chunk => { - dataChunks.push(chunk); - }); - client.on('end', () => { - if (!dataChunks.length) { - console.warn('[Vue Named Pipe Client] No response from server for request:', request.type); - resolve(undefined); - return; - } - const data = Buffer.concat(dataChunks); - const text = data.toString(); - let json = null; - try { - json = JSON.parse(text); - } catch (e) { - console.error('[Vue Named Pipe Client] Failed to parse response:', text); - resolve(undefined); - return; - } - resolve(json); - }); - client.write(JSON.stringify(request)); - }); -} diff --git a/packages/typescript-plugin/lib/common.ts b/packages/typescript-plugin/lib/common.ts index 1f9ca7e05..c75ea8635 100644 --- a/packages/typescript-plugin/lib/common.ts +++ b/packages/typescript-plugin/lib/common.ts @@ -8,12 +8,14 @@ export function decorateLanguageServiceForVue( languageService: ts.LanguageService, vueOptions: vue.VueCompilerOptions, ts: typeof import('typescript'), + isTsPlugin: boolean, ) { - - const getCompletionsAtPosition = languageService.getCompletionsAtPosition; - const getCompletionEntryDetails = languageService.getCompletionEntryDetails; - const getCodeFixesAtPosition = languageService.getCodeFixesAtPosition; - const getEncodedSemanticClassifications = languageService.getEncodedSemanticClassifications; + const { + getCompletionsAtPosition, + getCompletionEntryDetails, + getCodeFixesAtPosition, + getEncodedSemanticClassifications, + } = languageService; languageService.getCompletionsAtPosition = (fileName, position, options) => { const result = getCompletionsAtPosition(fileName, position, options); @@ -75,60 +77,85 @@ export function decorateLanguageServiceForVue( result = result.filter(entry => entry.description.indexOf('__VLS_') === -1); return result; }; - languageService.getEncodedSemanticClassifications = (fileName, span, format) => { - const result = getEncodedSemanticClassifications(fileName, span, format); - const file = files.get(fileName); - if ( - file?.generated?.code instanceof vue.VueGeneratedCode - && file.generated.code.sfc.template - ) { - const validComponentNames = _getComponentNames(ts, languageService, file.generated.code, vueOptions); - const components = new Set([ - ...validComponentNames, - ...validComponentNames.map(vue.hyphenateTag), - ]); - const { template } = file.generated.code.sfc; - const spanTemplateRange = [ - span.start - template.startTagEnd, - span.start + span.length - template.startTagEnd, - ] as const; - template.ast?.children.forEach(function visit(node) { - if (node.loc.end.offset <= spanTemplateRange[0] || node.loc.start.offset >= spanTemplateRange[1]) { - return; - } - if (node.type === 1 satisfies vue.CompilerDOM.NodeTypes.ELEMENT) { - if (components.has(node.tag)) { + if (isTsPlugin) { + languageService.getEncodedSemanticClassifications = (fileName, span, format) => { + const result = getEncodedSemanticClassifications(fileName, span, format); + const file = files.get(fileName); + if (file?.generated?.code instanceof vue.VueGeneratedCode) { + const { template } = file.generated.code.sfc; + if (template) { + for (const componentSpan of getComponentSpans.call( + { typescript: ts, languageService, vueOptions }, + file.generated.code, + template, + { + start: span.start - template.startTagEnd, + length: span.length, + }, + )) { result.spans.push( - node.loc.start.offset + node.loc.source.indexOf(node.tag) + template.startTagEnd, - node.tag.length, + componentSpan.start + template.startTagEnd, + componentSpan.length, 256, // class ); - if (template.lang === 'html' && !node.isSelfClosing) { - result.spans.push( - node.loc.start.offset + node.loc.source.lastIndexOf(node.tag) + template.startTagEnd, - node.tag.length, - 256, // class - ); - } - } - for (const child of node.children) { - visit(child); } } - else if (node.type === 9 satisfies vue.CompilerDOM.NodeTypes.IF) { - for (const branch of node.branches) { - for (const child of branch.children) { - visit(child); - } - } + } + return result; + }; + } +} + +export function getComponentSpans( + this: { + typescript: typeof import('typescript'); + languageService: ts.LanguageService; + vueOptions: vue.VueCompilerOptions; + }, + vueCode: vue.VueGeneratedCode, + template: NonNullable, + spanTemplateRange: ts.TextSpan, +) { + const { typescript: ts, languageService, vueOptions } = this; + const result: ts.TextSpan[] = []; + const validComponentNames = _getComponentNames(ts, languageService, vueCode, vueOptions); + const components = new Set([ + ...validComponentNames, + ...validComponentNames.map(vue.hyphenateTag), + ]); + template.ast?.children.forEach(function visit(node) { + if (node.loc.end.offset <= spanTemplateRange.start || node.loc.start.offset >= (spanTemplateRange.start + spanTemplateRange.length)) { + return; + } + if (node.type === 1 satisfies vue.CompilerDOM.NodeTypes.ELEMENT) { + if (components.has(node.tag)) { + result.push({ + start: node.loc.start.offset + node.loc.source.indexOf(node.tag), + length: node.tag.length, + }); + if (template.lang === 'html' && !node.isSelfClosing) { + result.push({ + start: node.loc.start.offset + node.loc.source.lastIndexOf(node.tag), + length: node.tag.length, + }); } - else if (node.type === 11 satisfies vue.CompilerDOM.NodeTypes.FOR) { - for (const child of node.children) { - visit(child); - } + } + for (const child of node.children) { + visit(child); + } + } + else if (node.type === 9 satisfies vue.CompilerDOM.NodeTypes.IF) { + for (const branch of node.branches) { + for (const child of branch.children) { + visit(child); } - }); + } } - return result; - }; + else if (node.type === 11 satisfies vue.CompilerDOM.NodeTypes.FOR) { + for (const child of node.children) { + visit(child); + } + } + }); + return result; } diff --git a/packages/typescript-plugin/lib/requests/collectExtractProps.ts b/packages/typescript-plugin/lib/requests/collectExtractProps.ts index e2655a49e..9ef7b0131 100644 --- a/packages/typescript-plugin/lib/requests/collectExtractProps.ts +++ b/packages/typescript-plugin/lib/requests/collectExtractProps.ts @@ -1,15 +1,18 @@ -import { VueGeneratedCode, isSemanticTokensEnabled } from '@vue/language-core'; -import { getProject } from '../utils'; +import { FileRegistry, VueGeneratedCode, isSemanticTokensEnabled } from '@vue/language-core'; import type * as ts from 'typescript'; -export function collectExtractProps(fileName: string, templateCodeRange: [number, number], isTsPlugin: boolean = true) { +export function collectExtractProps( + this: { + typescript: typeof import('typescript'); + languageService: ts.LanguageService; + files: FileRegistry; + isTsPlugin: boolean, + }, + fileName: string, + templateCodeRange: [number, number], +) { + const { typescript: ts, languageService, files, isTsPlugin } = this; - const match = getProject(fileName); - if (!match) { - return; - } - - const { info, files, ts } = match; const volarFile = files.get(fileName); if (!(volarFile?.generated?.code instanceof VueGeneratedCode)) { return; @@ -20,7 +23,6 @@ export function collectExtractProps(fileName: string, templateCodeRange: [number type: string; model: boolean; }>(); - const languageService = info.languageService; const program: ts.Program = (languageService as any).getCurrentProgram(); if (!program) { return; diff --git a/packages/typescript-plugin/lib/requests/componentInfos.ts b/packages/typescript-plugin/lib/requests/componentInfos.ts index a8c4ac7f8..d69f17432 100644 --- a/packages/typescript-plugin/lib/requests/componentInfos.ts +++ b/packages/typescript-plugin/lib/requests/componentInfos.ts @@ -1,27 +1,31 @@ import * as vue from '@vue/language-core'; import { camelize, capitalize } from '@vue/shared'; import type * as ts from 'typescript'; -import { getProject } from '../utils'; -export function getComponentProps(fileName: string, tag: string, requiredOnly = false) { - const match = getProject(fileName); - if (!match) { - return; - } - const { ts, files, vueOptions } = match; +export function getComponentProps( + this: { + typescript: typeof import('typescript'); + languageService: ts.LanguageService; + files: vue.FileRegistry; + vueOptions: vue.VueCompilerOptions, + }, + fileName: string, + tag: string, + requiredOnly = false, +) { + const { typescript: ts, files, vueOptions, languageService } = this; const volarFile = files.get(fileName); if (!(volarFile?.generated?.code instanceof vue.VueGeneratedCode)) { return; } const vueCode = volarFile.generated.code; - const tsLs = match.info.languageService; - const program: ts.Program = (tsLs as any).getCurrentProgram(); + const program: ts.Program = (languageService as any).getCurrentProgram(); if (!program) { return; } const checker = program.getTypeChecker(); - const components = getVariableType(ts, tsLs, vueCode, '__VLS_components'); + const components = getVariableType(ts, languageService, vueCode, '__VLS_components'); if (!components) { return []; } @@ -86,25 +90,29 @@ export function getComponentProps(fileName: string, tag: string, requiredOnly = return [...result]; } -export function getComponentEvents(fileName: string, tag: string) { - const match = getProject(fileName); - if (!match) { - return; - } - const { ts, files, vueOptions } = match; +export function getComponentEvents( + this: { + typescript: typeof import('typescript'); + languageService: ts.LanguageService; + files: vue.FileRegistry; + vueOptions: vue.VueCompilerOptions, + }, + fileName: string, + tag: string, +) { + const { typescript: ts, files, vueOptions, languageService } = this; const volarFile = files.get(fileName); if (!(volarFile?.generated?.code instanceof vue.VueGeneratedCode)) { return; } - const tsLs = match.info.languageService; const vueCode = volarFile.generated.code; - const program: ts.Program = (tsLs as any).getCurrentProgram(); + const program: ts.Program = (languageService as any).getCurrentProgram(); if (!program) { return; } const checker = program.getTypeChecker(); - const components = getVariableType(ts, tsLs, vueCode, '__VLS_components'); + const components = getVariableType(ts, languageService, vueCode, '__VLS_components'); if (!components) { return []; } @@ -163,39 +171,44 @@ export function getComponentEvents(fileName: string, tag: string) { return [...result]; } -export function getTemplateContextProps(fileName: string) { - const match = getProject(fileName); - if (!match) { - return; - } - const { ts, files } = match; +export function getTemplateContextProps( + this: { + typescript: typeof import('typescript'); + languageService: ts.LanguageService; + files: vue.FileRegistry; + }, + fileName: string, +) { + const { typescript: ts, files, languageService } = this; const volarFile = files.get(fileName); if (!(volarFile?.generated?.code instanceof vue.VueGeneratedCode)) { return; } - const tsLs = match.info.languageService; const vueCode = volarFile.generated.code; - return getVariableType(ts, tsLs, vueCode, '__VLS_ctx') + return getVariableType(ts, languageService, vueCode, '__VLS_ctx') ?.type ?.getProperties() .map(c => c.name); } -export function getComponentNames(fileName: string) { - const match = getProject(fileName); - if (!match) { - return; - } - const { ts, files, vueOptions } = match; +export function getComponentNames( + this: { + typescript: typeof import('typescript'); + languageService: ts.LanguageService; + files: vue.FileRegistry; + vueOptions: vue.VueCompilerOptions, + }, + fileName: string, +) { + const { typescript: ts, files, vueOptions, languageService } = this; const volarFile = files.get(fileName); if (!(volarFile?.generated?.code instanceof vue.VueGeneratedCode)) { return; } - const tsLs = match.info.languageService; const vueCode = volarFile.generated.code; - return getVariableType(ts, tsLs, vueCode, '__VLS_components') + return getVariableType(ts, languageService, vueCode, '__VLS_components') ?.type ?.getProperties() .map(c => c.name) @@ -219,18 +232,21 @@ export function _getComponentNames( ?? []; } -export function getElementAttrs(fileName: string, tagName: string) { - const match = getProject(fileName); - if (!match) { - return; - } - const { ts, files } = match; +export function getElementAttrs( + this: { + typescript: typeof import('typescript'); + languageService: ts.LanguageService; + files: vue.FileRegistry; + }, + fileName: string, + tagName: string, +) { + const { typescript: ts, files, languageService } = this; const volarFile = files.get(fileName); if (!(volarFile?.generated?.code instanceof vue.VueGeneratedCode)) { return; } - const tsLs = match.info.languageService; - const program: ts.Program = (tsLs as any).getCurrentProgram(); + const program: ts.Program = (languageService as any).getCurrentProgram(); if (!program) { return; } diff --git a/packages/typescript-plugin/lib/requests/containsFile.ts b/packages/typescript-plugin/lib/requests/containsFile.ts deleted file mode 100644 index d191f258a..000000000 --- a/packages/typescript-plugin/lib/requests/containsFile.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { getProject } from '../utils'; - -export function containsFile(fileName: string) { - return !!getProject(fileName); -} diff --git a/packages/typescript-plugin/lib/requests/getPropertiesAtLocation.ts b/packages/typescript-plugin/lib/requests/getPropertiesAtLocation.ts index fd39eded6..43a2eba3c 100644 --- a/packages/typescript-plugin/lib/requests/getPropertiesAtLocation.ts +++ b/packages/typescript-plugin/lib/requests/getPropertiesAtLocation.ts @@ -1,16 +1,17 @@ -import { isCompletionEnabled } from '@vue/language-core'; -import { getProject } from '../utils'; +import { FileRegistry, isCompletionEnabled } from '@vue/language-core'; import type * as ts from 'typescript'; -export function getPropertiesAtLocation(fileName: string, position: number, isTsPlugin: boolean = true) { - - const match = getProject(fileName); - if (!match) { - return; - } - - const { info, files, ts } = match; - const languageService = info.languageService; +export function getPropertiesAtLocation( + this: { + typescript: typeof import('typescript'); + languageService: ts.LanguageService; + files: FileRegistry; + isTsPlugin: boolean, + }, + fileName: string, + position: number, +) { + const { languageService, files, typescript: ts, isTsPlugin } = this; // mapping const file = files.get(fileName); diff --git a/packages/typescript-plugin/lib/requests/getQuickInfoAtPosition.ts b/packages/typescript-plugin/lib/requests/getQuickInfoAtPosition.ts index 7a1015f06..d4c3162bb 100644 --- a/packages/typescript-plugin/lib/requests/getQuickInfoAtPosition.ts +++ b/packages/typescript-plugin/lib/requests/getQuickInfoAtPosition.ts @@ -1,14 +1,11 @@ -import { getProject } from '../utils'; - -export function getQuickInfoAtPosition(fileName: string, position: number) { - - const match = getProject(fileName); - if (!match) { - return; - } - - const { info } = match; - const languageService = info.languageService; - +import type * as ts from 'typescript'; +export function getQuickInfoAtPosition( + this: { + languageService: ts.LanguageService; + }, + fileName: string, + position: number, +) { + const { languageService } = this; return languageService.getQuickInfoAtPosition(fileName, position); } diff --git a/packages/typescript-plugin/lib/server.ts b/packages/typescript-plugin/lib/server.ts index d288684b2..521a84efa 100644 --- a/packages/typescript-plugin/lib/server.ts +++ b/packages/typescript-plugin/lib/server.ts @@ -3,10 +3,10 @@ import * as net from 'net'; import type * as ts from 'typescript'; import { collectExtractProps } from './requests/collectExtractProps'; import { getComponentEvents, getComponentNames, getComponentProps, getElementAttrs, getTemplateContextProps } from './requests/componentInfos'; -import { containsFile } from './requests/containsFile'; import { getPropertiesAtLocation } from './requests/getPropertiesAtLocation'; import { getQuickInfoAtPosition } from './requests/getQuickInfoAtPosition'; import { NamedPipeServer, connect, pipeTable } from './utils'; +import type { FileRegistry, VueCompilerOptions } from '@vue/language-core'; export interface Request { type: 'containsFile' @@ -19,13 +19,16 @@ export interface Request { | 'getTemplateContextProps' | 'getComponentNames' | 'getElementAttrs'; - args: any; + args: [fileName: string, ...rest: any]; } let started = false; -export function startNamedPipeServer(serverKind: ts.server.ProjectKind, currentDirectory: string) { - +export function startNamedPipeServer( + ts: typeof import('typescript'), + serverKind: ts.server.ProjectKind, + currentDirectory: string, +) { if (started) { return; } @@ -38,45 +41,59 @@ export function startNamedPipeServer(serverKind: ts.server.ProjectKind, currentD connection.on('data', data => { const text = data.toString(); const request: Request = JSON.parse(text); - if (request.type === 'containsFile') { - const result = containsFile.apply(null, request.args); - connection.write(JSON.stringify(result ?? null)); - } - else if (request.type === 'collectExtractProps') { - const result = collectExtractProps.apply(null, request.args); - connection.write(JSON.stringify(result ?? null)); - } - else if (request.type === 'getPropertiesAtLocation') { - const result = getPropertiesAtLocation.apply(null, request.args); - connection.write(JSON.stringify(result ?? null)); - } - else if (request.type === 'getQuickInfoAtPosition') { - const result = getQuickInfoAtPosition.apply(null, request.args); - connection.write(JSON.stringify(result ?? null)); - } - // Component Infos - else if (request.type === 'getComponentProps') { - const result = getComponentProps.apply(null, request.args); - connection.write(JSON.stringify(result ?? null)); - } - else if (request.type === 'getComponentEvents') { - const result = getComponentEvents.apply(null, request.args); - connection.write(JSON.stringify(result ?? null)); - } - else if (request.type === 'getTemplateContextProps') { - const result = getTemplateContextProps.apply(null, request.args); - connection.write(JSON.stringify(result ?? null)); - } - else if (request.type === 'getComponentNames') { - const result = getComponentNames.apply(null, request.args); - connection.write(JSON.stringify(result ?? null)); - } - else if (request.type === 'getElementAttrs') { - const result = getElementAttrs.apply(null, request.args); - connection.write(JSON.stringify(result ?? null)); + const fileName = request.args[0]; + const project = getProject(fileName); + if (project) { + const requestContext = { + typescript: ts, + languageService: project.info.languageService, + files: project.files, + vueOptions: project.vueOptions, + isTsPlugin: true, + }; + if (request.type === 'containsFile') { + const result = !!getProject(fileName); + connection.write(JSON.stringify(result ?? null)); + } + else if (request.type === 'collectExtractProps') { + const result = collectExtractProps.apply(requestContext, request.args as any); + connection.write(JSON.stringify(result ?? null)); + } + else if (request.type === 'getPropertiesAtLocation') { + const result = getPropertiesAtLocation.apply(requestContext, request.args as any); + connection.write(JSON.stringify(result ?? null)); + } + else if (request.type === 'getQuickInfoAtPosition') { + const result = getQuickInfoAtPosition.apply(requestContext, request.args as any); + connection.write(JSON.stringify(result ?? null)); + } + // Component Infos + else if (request.type === 'getComponentProps') { + const result = getComponentProps.apply(requestContext, request.args as any); + connection.write(JSON.stringify(result ?? null)); + } + else if (request.type === 'getComponentEvents') { + const result = getComponentEvents.apply(requestContext, request.args as any); + connection.write(JSON.stringify(result ?? null)); + } + else if (request.type === 'getTemplateContextProps') { + const result = getTemplateContextProps.apply(requestContext, request.args as any); + connection.write(JSON.stringify(result ?? null)); + } + else if (request.type === 'getComponentNames') { + const result = getComponentNames.apply(requestContext, request.args as any); + connection.write(JSON.stringify(result ?? null)); + } + else if (request.type === 'getElementAttrs') { + const result = getElementAttrs.apply(requestContext, request.args as any); + connection.write(JSON.stringify(result ?? null)); + } + else { + console.warn('[Vue Named Pipe Server] Unknown request type:', request.type); + } } else { - console.warn('[Vue Named Pipe Server] Unknown request type:', request.type); + console.warn('[Vue Named Pipe Server] No project found for:', fileName); } connection.end(); }); @@ -120,3 +137,17 @@ function cleanupPipeTable() { }); } } + +export const projects = new Map(); + +function getProject(fileName: string) { + for (const [project, data] of projects) { + if (project.containsFile(fileName as ts.server.NormalizedPath)) { + return data; + } + } +} diff --git a/packages/typescript-plugin/lib/utils.ts b/packages/typescript-plugin/lib/utils.ts index 9d9c79915..746f3cadc 100644 --- a/packages/typescript-plugin/lib/utils.ts +++ b/packages/typescript-plugin/lib/utils.ts @@ -1,8 +1,9 @@ -import type { FileRegistry, VueCompilerOptions } from '@vue/language-core'; import * as os from 'os'; import * as net from 'net'; import * as path from 'path'; import type * as ts from 'typescript'; +import * as fs from 'fs'; +import type { Request } from './server'; export interface NamedPipeServer { path: string; @@ -14,21 +15,6 @@ const { version } = require('../package.json'); export const pipeTable = path.join(os.tmpdir(), `vue-tsp-table-${version}.json`); -export const projects = new Map(); - -export function getProject(fileName: string) { - for (const [project, data] of projects) { - if (project.containsFile(fileName as ts.server.NormalizedPath)) { - return data; - } - } -} - export function connect(path: string) { return new Promise(resolve => { const client = net.connect(path); @@ -40,3 +26,60 @@ export function connect(path: string) { }); }); } + +export async function searchNamedPipeServerForFile(fileName: string) { + if (!fs.existsSync(pipeTable)) { + return; + } + const servers: NamedPipeServer[] = JSON.parse(fs.readFileSync(pipeTable, 'utf8')); + const configuredServers = servers + .filter(item => item.serverKind === 1 satisfies ts.server.ProjectKind.Configured); + const inferredServers = servers + .filter(item => item.serverKind === 0 satisfies ts.server.ProjectKind.Inferred) + .sort((a, b) => b.currentDirectory.length - a.currentDirectory.length); + for (const server of configuredServers) { + const client = await connect(server.path); + if (client) { + const response = await sendRequestWorker({ type: 'containsFile', args: [fileName] }, client); + if (response) { + return server; + } + } + } + for (const server of inferredServers) { + if (!path.relative(server.currentDirectory, fileName).startsWith('..')) { + const client = await connect(server.path); + if (client) { + return server; + } + } + } +} + +export function sendRequestWorker(request: Request, client: net.Socket) { + return new Promise(resolve => { + let dataChunks: Buffer[] = []; + client.on('data', chunk => { + dataChunks.push(chunk); + }); + client.on('end', () => { + if (!dataChunks.length) { + console.warn('[Vue Named Pipe Client] No response from server for request:', request.type); + resolve(undefined); + return; + } + const data = Buffer.concat(dataChunks); + const text = data.toString(); + let json = null; + try { + json = JSON.parse(text); + } catch (e) { + console.error('[Vue Named Pipe Client] Failed to parse response:', text); + resolve(undefined); + return; + } + resolve(json); + }); + client.write(JSON.stringify(request)); + }); +}