From 025906a79b431319eb072952572f7d69eafbb1e6 Mon Sep 17 00:00:00 2001 From: KazariEX Date: Sun, 28 Sep 2025 20:31:58 +0800 Subject: [PATCH 1/4] refactor: move reactivity analysis logic to a seperate typescript plugin --- extensions/vscode/index.ts | 4 ++ .../vscode/lib/reactivityVisualization.ts | 6 +- extensions/vscode/package.json | 3 + .../vscode/reactivityAnalysis/plugin.ts | 61 +++++++++++++++++-- extensions/vscode/rolldown.config.ts | 7 +++ extensions/vscode/tsconfig.json | 2 +- packages/typescript-plugin/index.ts | 15 ----- .../typescript-plugin/lib/requests/index.ts | 4 -- packages/typescript-plugin/package.json | 1 - pnpm-lock.yaml | 12 +++- 10 files changed, 83 insertions(+), 32 deletions(-) rename packages/typescript-plugin/lib/requests/getReactiveReferences.ts => extensions/vscode/reactivityAnalysis/plugin.ts (58%) diff --git a/extensions/vscode/index.ts b/extensions/vscode/index.ts index 1762778c86..e3dd535148 100644 --- a/extensions/vscode/index.ts +++ b/extensions/vscode/index.ts @@ -240,6 +240,10 @@ function patchTypeScriptExtension() { configNamespace: 'typescript', languages: config.server.includeLanguages, }, + { + name: 'vue-reactivity-analysis-plugin-pack', + enableForWorkspaceTypeScriptVersions: true, + }, ]; fs.readFileSync = (...args: any[]) => { diff --git a/extensions/vscode/lib/reactivityVisualization.ts b/extensions/vscode/lib/reactivityVisualization.ts index d8f0897be8..4694e24cd7 100644 --- a/extensions/vscode/lib/reactivityVisualization.ts +++ b/extensions/vscode/lib/reactivityVisualization.ts @@ -1,6 +1,6 @@ -import type { getReactiveReferences } from '@vue/typescript-plugin/lib/requests/getReactiveReferences'; import type * as ts from 'typescript'; import * as vscode from 'vscode'; +import type { ReactivityAnalysisReturns } from '../reactivityAnalysis/plugin'; import { config } from './config'; const dependencyDecorations = vscode.window.createTextEditorDecorationType({ @@ -85,11 +85,11 @@ export function activate( try { const result = await vscode.commands.executeCommand< { - body?: ReturnType; + body?: ReactivityAnalysisReturns; } | undefined >( 'typescript.tsserverRequest', - '_vue:getReactiveReferences', + '_vue:getReactivityAnalysis', [ document.uri.fsPath.replace(/\\/g, '/'), document.offsetAt(editor.selection.active), diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index c536717d70..a27e5163a0 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -461,11 +461,14 @@ "@types/node": "^22.10.4", "@types/semver": "^7.5.3", "@types/vscode": "1.88.0", + "@volar/typescript": "2.4.23", "@volar/vscode": "2.4.23", "@vscode/vsce": "^3.2.1", "@vue/compiler-sfc": "^3.5.0", + "@vue/language-core": "3.0.8", "@vue/language-server": "3.0.8", "@vue/typescript-plugin": "3.0.8", + "laplacenoma": "^0.0.3", "reactive-vscode": "^0.2.9", "rolldown": "1.0.0-beta.8", "semver": "^7.5.4", diff --git a/packages/typescript-plugin/lib/requests/getReactiveReferences.ts b/extensions/vscode/reactivityAnalysis/plugin.ts similarity index 58% rename from packages/typescript-plugin/lib/requests/getReactiveReferences.ts rename to extensions/vscode/reactivityAnalysis/plugin.ts index a2220c1476..46d551b6a2 100644 --- a/packages/typescript-plugin/lib/requests/getReactiveReferences.ts +++ b/extensions/vscode/reactivityAnalysis/plugin.ts @@ -1,9 +1,58 @@ import { createProxyLanguageService, decorateLanguageServiceHost } from '@volar/typescript'; import type { Language, SourceScript } from '@vue/language-core'; import { createAnalyzer } from 'laplacenoma'; -import * as rulesVue from 'laplacenoma/rules/vue'; +// @ts-expect-error +import rulesVue from 'laplacenoma/rules/vue'; import type * as ts from 'typescript'; +const plugin: ts.server.PluginModuleFactory = module => { + const { typescript: ts } = module; + + return { + create(info) { + if (!info.session || (info.session as any).handlers.has('_vue:getReactivityAnalysis')) { + return info.languageService; + } + + info.session.addProtocolHandler('_vue:getReactivityAnalysis', request => { + const [fileName, position] = request.arguments; + // @ts-expect-error + const { project } = info.session.getFileAndProject({ + file: fileName, + projectFileName: undefined, + }) as { + file: ts.server.NormalizedPath; + project: ts.server.Project; + }; + + let response; + const language = project['program']?.__vue__?.language as Language | undefined; + if (language) { + const sourceScript = language.scripts.get(fileName); + if (sourceScript) { + response = getReactivityAnalysis( + ts, + language, + sourceScript, + position, + sourceScript.generated ? sourceScript.snapshot.getLength() : 0, + ); + } + } + + return { + response, + responseRequired: true, + }; + }); + + return info.languageService; + }, + }; +}; + +module.exports = plugin; + const analyzer = createAnalyzer({ rules: rulesVue, }); @@ -12,8 +61,11 @@ let currentVersion = -1; let currentFileName = ''; let currentSnapshot: ts.IScriptSnapshot | undefined; let languageService: ts.LanguageService | undefined; +let languageServiceHost: ts.LanguageServiceHost | undefined; + +export type ReactivityAnalysisReturns = ReturnType; -export function getReactiveReferences( +function getReactivityAnalysis( ts: typeof import('typescript'), language: Language, sourceScript: SourceScript, @@ -26,13 +78,12 @@ export function getReactiveReferences( currentVersion++; } if (!languageService) { - const compilerOptions: ts.CompilerOptions = { allowJs: true, allowNonTsExtensions: true }; - const languageServiceHost: ts.LanguageServiceHost = { + languageServiceHost = { getProjectVersion: () => currentVersion.toString(), getScriptVersion: () => currentVersion.toString(), getScriptFileNames: () => [currentFileName], getScriptSnapshot: fileName => fileName === currentFileName ? currentSnapshot : undefined, - getCompilationSettings: () => compilerOptions, + getCompilationSettings: () => ({ allowJs: true, allowNonTsExtensions: true }), getCurrentDirectory: () => '', getDefaultLibFileName: () => '', readFile: () => undefined, diff --git a/extensions/vscode/rolldown.config.ts b/extensions/vscode/rolldown.config.ts index 3d4e3cd38e..2ad6c45ae9 100644 --- a/extensions/vscode/rolldown.config.ts +++ b/extensions/vscode/rolldown.config.ts @@ -8,6 +8,7 @@ const resolve = (...paths: string[]) => path.resolve(__dirname, ...paths); const config: RolldownOptions = { input: { 'extension': './index.ts', + 'reactivity-analysis': './reactivityAnalysis/plugin.ts', }, output: { format: 'cjs', @@ -33,6 +34,12 @@ const config: RolldownOptions = { `module.exports = require('../../dist/typescript-plugin.js');`, ); + fs.mkdirSync(resolve('./node_modules/vue-reactivity-analysis-plugin-pack'), { recursive: true }); + fs.writeFileSync( + resolve('./node_modules/vue-reactivity-analysis-plugin-pack/index.js'), + `module.exports = require('../../dist/reactivity-analysis.js');`, + ); + if (isDev) { fs.mkdirSync(resolve('./dist'), { recursive: true }); fs.writeFileSync( diff --git a/extensions/vscode/tsconfig.json b/extensions/vscode/tsconfig.json index 63104e2a23..ae7b674395 100644 --- a/extensions/vscode/tsconfig.json +++ b/extensions/vscode/tsconfig.json @@ -5,5 +5,5 @@ "module": "CommonJS", "moduleResolution": "Node" }, - "include": ["*", "lib/**/*"] + "include": ["*", "lib/**/*", "reactivityAnalysis/plugin.ts"] } diff --git a/packages/typescript-plugin/index.ts b/packages/typescript-plugin/index.ts index 198290cb91..8e2a9850d9 100644 --- a/packages/typescript-plugin/index.ts +++ b/packages/typescript-plugin/index.ts @@ -12,7 +12,6 @@ import { getComponentSlots } from './lib/requests/getComponentSlots'; import { getElementAttrs } from './lib/requests/getElementAttrs'; import { getElementNames } from './lib/requests/getElementNames'; import { getImportPathForFile } from './lib/requests/getImportPathForFile'; -import { getReactiveReferences } from './lib/requests/getReactiveReferences'; import { isRefAtPosition } from './lib/requests/isRefAtPosition'; const windowsPathReg = /\\/g; @@ -187,20 +186,6 @@ export = createLanguageServicePlugin( const { languageService } = getLanguageService(fileName); return createResponse(getElementNames(ts, languageService.getProgram()!, fileName)); }); - session.addProtocolHandler('_vue:getReactiveReferences', request => { - const [fileName, position]: Parameters = request.arguments; - const { language } = getLanguageService(fileName); - const sourceScript = language.scripts.get(fileName)!; - return createResponse( - getReactiveReferences( - ts, - language, - sourceScript, - position, - sourceScript.generated ? sourceScript.snapshot.getLength() : 0, - ), - ); - }); projectService.logger.info('Vue specific commands are successfully added.'); diff --git a/packages/typescript-plugin/lib/requests/index.ts b/packages/typescript-plugin/lib/requests/index.ts index 97ed8c2b2d..4cf06bf368 100644 --- a/packages/typescript-plugin/lib/requests/index.ts +++ b/packages/typescript-plugin/lib/requests/index.ts @@ -40,10 +40,6 @@ export interface Requests { getElementNames( fileName: string, ): Response>; - getReactiveReferences( - fileName: string, - position: number, - ): Response>; getDocumentHighlights( fileName: string, position: number, diff --git a/packages/typescript-plugin/package.json b/packages/typescript-plugin/package.json index 8f5cda0648..c982712545 100644 --- a/packages/typescript-plugin/package.json +++ b/packages/typescript-plugin/package.json @@ -16,7 +16,6 @@ "@volar/typescript": "2.4.23", "@vue/language-core": "3.0.8", "@vue/shared": "^3.5.0", - "laplacenoma": "^0.0.3", "path-browserify": "^1.0.1" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e12d17eba9..0da1c5f581 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: '@types/vscode': specifier: 1.88.0 version: 1.88.0 + '@volar/typescript': + specifier: 2.4.23 + version: 2.4.23 '@volar/vscode': specifier: 2.4.23 version: 2.4.23 @@ -56,12 +59,18 @@ importers: '@vue/compiler-sfc': specifier: ^3.5.0 version: 3.5.13 + '@vue/language-core': + specifier: 3.0.8 + version: link:../../packages/language-core '@vue/language-server': specifier: 3.0.8 version: link:../../packages/language-server '@vue/typescript-plugin': specifier: 3.0.8 version: link:../../packages/typescript-plugin + laplacenoma: + specifier: ^0.0.3 + version: 0.0.3 reactive-vscode: specifier: ^0.2.9 version: 0.2.14(@types/vscode@1.88.0) @@ -286,9 +295,6 @@ importers: '@vue/shared': specifier: ^3.5.0 version: 3.5.13 - laplacenoma: - specifier: ^0.0.3 - version: 0.0.3 path-browserify: specifier: ^1.0.1 version: 1.0.1 From 655e270810c1e3777868e54167b863b24c05533c Mon Sep 17 00:00:00 2001 From: KazariEX Date: Sun, 28 Sep 2025 20:34:00 +0800 Subject: [PATCH 2/4] chore: remove `getReactiveReferences` in language server --- packages/language-server/index.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/language-server/index.ts b/packages/language-server/index.ts index a64c3b07b6..8752611f96 100644 --- a/packages/language-server/index.ts +++ b/packages/language-server/index.ts @@ -122,9 +122,6 @@ connection.onInitialize(params => { getImportPathForFile(...args) { return sendTsServerRequest('_vue:getImportPathForFile', args); }, - getReactiveReferences(...args) { - return sendTsServerRequest('_vue:getReactiveReferences', args); - }, isRefAtPosition(...args) { return sendTsServerRequest('_vue:isRefAtPosition', args); }, From e1aa29d402baa7cb855bcbc7eb1e14d2ef5dfd40 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Sun, 28 Sep 2025 20:50:25 +0800 Subject: [PATCH 3/4] refactor --- .../vscode/lib/reactivityVisualization.ts | 4 +- .../vscode/reactivityAnalysis/plugin.ts | 92 ++++++++----------- packages/typescript-plugin/index.ts | 3 +- 3 files changed, 41 insertions(+), 58 deletions(-) diff --git a/extensions/vscode/lib/reactivityVisualization.ts b/extensions/vscode/lib/reactivityVisualization.ts index 4694e24cd7..c84a55cdb8 100644 --- a/extensions/vscode/lib/reactivityVisualization.ts +++ b/extensions/vscode/lib/reactivityVisualization.ts @@ -1,6 +1,6 @@ +import type { Analyzer } from 'laplacenoma'; import type * as ts from 'typescript'; import * as vscode from 'vscode'; -import type { ReactivityAnalysisReturns } from '../reactivityAnalysis/plugin'; import { config } from './config'; const dependencyDecorations = vscode.window.createTextEditorDecorationType({ @@ -85,7 +85,7 @@ export function activate( try { const result = await vscode.commands.executeCommand< { - body?: ReactivityAnalysisReturns; + body?: ReturnType | undefined; } | undefined >( 'typescript.tsserverRequest', diff --git a/extensions/vscode/reactivityAnalysis/plugin.ts b/extensions/vscode/reactivityAnalysis/plugin.ts index 46d551b6a2..d265313862 100644 --- a/extensions/vscode/reactivityAnalysis/plugin.ts +++ b/extensions/vscode/reactivityAnalysis/plugin.ts @@ -1,84 +1,67 @@ import { createProxyLanguageService, decorateLanguageServiceHost } from '@volar/typescript'; -import type { Language, SourceScript } from '@vue/language-core'; +import type { Language } from '@vue/language-core'; import { createAnalyzer } from 'laplacenoma'; // @ts-expect-error import rulesVue from 'laplacenoma/rules/vue'; import type * as ts from 'typescript'; -const plugin: ts.server.PluginModuleFactory = module => { - const { typescript: ts } = module; +let currentVersion = -1; +let currentFileName = ''; +let currentSnapshot: ts.IScriptSnapshot | undefined; +let languageService: ts.LanguageService | undefined; +const analyzer = createAnalyzer({ rules: rulesVue }); +const plugin: ts.server.PluginModuleFactory = ({ typescript: ts }) => { return { create(info) { - if (!info.session || (info.session as any).handlers.has('_vue:getReactivityAnalysis')) { - return info.languageService; + if (info.session && !(info.session as any).handlers.has('_vue:getReactivityAnalysis')) { + info.session.addProtocolHandler('_vue:getReactivityAnalysis', request => { + const [fileName, position]: [string, number] = request.arguments; + return { + response: getReactivityAnalysis(ts, info.session!, fileName, position), + responseRequired: true, + }; + }); } - info.session.addProtocolHandler('_vue:getReactivityAnalysis', request => { - const [fileName, position] = request.arguments; - // @ts-expect-error - const { project } = info.session.getFileAndProject({ - file: fileName, - projectFileName: undefined, - }) as { - file: ts.server.NormalizedPath; - project: ts.server.Project; - }; - - let response; - const language = project['program']?.__vue__?.language as Language | undefined; - if (language) { - const sourceScript = language.scripts.get(fileName); - if (sourceScript) { - response = getReactivityAnalysis( - ts, - language, - sourceScript, - position, - sourceScript.generated ? sourceScript.snapshot.getLength() : 0, - ); - } - } - - return { - response, - responseRequired: true, - }; - }); - return info.languageService; }, }; }; -module.exports = plugin; - -const analyzer = createAnalyzer({ - rules: rulesVue, -}); - -let currentVersion = -1; -let currentFileName = ''; -let currentSnapshot: ts.IScriptSnapshot | undefined; -let languageService: ts.LanguageService | undefined; -let languageServiceHost: ts.LanguageServiceHost | undefined; - -export type ReactivityAnalysisReturns = ReturnType; +export = plugin; function getReactivityAnalysis( ts: typeof import('typescript'), - language: Language, - sourceScript: SourceScript, + session: ts.server.Session, + fileName: string, position: number, - leadingOffset: number = 0, ) { + const { project } = session['getFileAndProject']({ + file: fileName, + projectFileName: undefined, + }) as { + file: ts.server.NormalizedPath; + project: ts.server.Project; + }; + + const language: Language | undefined = project['program']?.__vue__?.language; + if (!language) { + return; + } + + const sourceScript = language.scripts.get(fileName); + if (!sourceScript) { + return; + } + if (currentSnapshot !== sourceScript.snapshot || currentFileName !== sourceScript.id) { currentSnapshot = sourceScript.snapshot; currentFileName = sourceScript.id; currentVersion++; } if (!languageService) { - languageServiceHost = { + const languageServiceHost: ts.LanguageServiceHost = { getProjectVersion: () => currentVersion.toString(), getScriptVersion: () => currentVersion.toString(), getScriptFileNames: () => [currentFileName], @@ -100,6 +83,7 @@ function getReactivityAnalysis( sourceScript.generated.root, ); const map = serviceScript ? language.maps.get(serviceScript.code, sourceScript) : undefined; + const leadingOffset = sourceScript.generated ? sourceScript.snapshot.getLength() : 0; const toSourceRange = map ? (pos: number, end: number) => { for (const [mappedStart, mappedEnd] of map.toSourceRange(pos - leadingOffset, end - leadingOffset, false)) { diff --git a/packages/typescript-plugin/index.ts b/packages/typescript-plugin/index.ts index 8e2a9850d9..5adfd21d9f 100644 --- a/packages/typescript-plugin/index.ts +++ b/packages/typescript-plugin/index.ts @@ -214,8 +214,7 @@ export = createLanguageServicePlugin( } function getLanguageService(fileName: string) { - // @ts-expect-error - const { project } = session.getFileAndProject({ + const { project } = session['getFileAndProject']({ file: fileName, projectFileName: undefined, }) as { From 6f8c5d73572ecb73095afb6d2bfb3a16d3f53b5e Mon Sep 17 00:00:00 2001 From: KazariEX Date: Sun, 28 Sep 2025 20:55:22 +0800 Subject: [PATCH 4/4] chore: tsconfig include --- extensions/vscode/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/vscode/tsconfig.json b/extensions/vscode/tsconfig.json index ae7b674395..2b105808f0 100644 --- a/extensions/vscode/tsconfig.json +++ b/extensions/vscode/tsconfig.json @@ -5,5 +5,5 @@ "module": "CommonJS", "moduleResolution": "Node" }, - "include": ["*", "lib/**/*", "reactivityAnalysis/plugin.ts"] + "include": ["*", "lib/**/*", "reactivityAnalysis/**/*"] }