From 1993c9ae7b0a686c8554b801b921e06dcd6b83ae Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Fri, 25 Mar 2022 16:25:54 +0800 Subject: [PATCH] fix: memory leak on vue-tsc watch close #1106 --- packages/vue-tsc/src/apis.ts | 2 +- packages/vue-tsc/src/proxy.ts | 71 ++++++++++++++----- .../vue-typescript/src/typescriptRuntime.ts | 11 ++- 3 files changed, 61 insertions(+), 23 deletions(-) diff --git a/packages/vue-tsc/src/apis.ts b/packages/vue-tsc/src/apis.ts index 346003849..6c5adcb8c 100644 --- a/packages/vue-tsc/src/apis.ts +++ b/packages/vue-tsc/src/apis.ts @@ -135,7 +135,7 @@ export function register( } } else { - file = ts.createSourceFile(tsOrVueLoc.fileName, docText, tsOrVueLoc.fileName.endsWith('.vue') ? ts.ScriptTarget.JSON : ts.ScriptTarget.Latest) + file = ts.createSourceFile(tsOrVueLoc.fileName, docText, tsOrVueLoc.fileName.endsWith('.vue') ? ts.ScriptTarget.JSON : ts.ScriptTarget.Latest); } } const newDiagnostic: T = { diff --git a/packages/vue-tsc/src/proxy.ts b/packages/vue-tsc/src/proxy.ts index d6e019521..7a306cf3e 100644 --- a/packages/vue-tsc/src/proxy.ts +++ b/packages/vue-tsc/src/proxy.ts @@ -4,6 +4,8 @@ import * as apis from './apis'; import { createTypeScriptRuntime } from '@volar/vue-typescript'; import { tsShared } from '@volar/vue-typescript'; +let projectVersion = 0; + export function createProgramProxy( options: ts.CreateProgramOptions, // rootNamesOrOptions: readonly string[] | CreateProgramOptions, _options?: ts.CompilerOptions, @@ -18,9 +20,13 @@ export function createProgramProxy( if (!options.host) return doThrow('!options.host'); + projectVersion++; + const host = options.host; const vueCompilerOptions = getVueCompilerOptions(); const scripts = new Map(); @@ -30,14 +36,21 @@ export function createProgramProxy( writeFile: undefined, getCompilationSettings: () => options.options, getVueCompilationSettings: () => vueCompilerOptions, - getScriptFileNames: () => options.rootNames as string[], - getScriptVersion: (fileName) => scripts.get(fileName)?.version ?? '', + getScriptFileNames: () => { + return options.rootNames as string[]; + }, + getScriptVersion, getScriptSnapshot, - getProjectVersion: () => '', - getVueProjectVersion: () => '', + getProjectVersion: () => { + return projectVersion.toString(); + }, + getVueProjectVersion: () => { + return projectVersion.toString(); + }, getProjectReferences: () => options.projectReferences, }; - const tsRuntime = createTypeScriptRuntime({ + + const tsRuntime = (options.oldProgram as any)?.__VLS_tsRuntime ?? createTypeScriptRuntime({ typescript: ts, baseCssModuleType: 'any', getCssClasses: () => ({}), @@ -45,23 +58,28 @@ export function createProgramProxy( vueLsHost: vueLsHost, isVueTsc: true, }); + tsRuntime.update(true); // must update before getProgram() to update virtual scripts + const tsProgram = tsRuntime.getTsLs('script').getProgram(); - if (!tsProgram) throw '!tsProgram'; + if (!tsProgram) + throw '!tsProgram'; - const tsProgramApis_2 = apis.register(ts, tsRuntime); - const tsProgramProxy = new Proxy(tsProgram, { - get: (target: any, property: keyof typeof tsProgramApis_2) => { + const proxyApis = apis.register(ts, tsRuntime); + const program = new Proxy(tsProgram, { + get: (target: any, property: keyof typeof proxyApis) => { tsRuntime.update(true); - return tsProgramApis_2[property] || target[property]; + return proxyApis[property] || target[property]; }, }); + (program as any).__VLS_tsRuntime = tsRuntime; + for (const rootName of options.rootNames) { // register file watchers host.getSourceFile(rootName, ts.ScriptTarget.ESNext); } - return tsProgramProxy; + return program; function getVueCompilerOptions(): vue.VueCompilerOptions { const tsConfig = options.options.configFilePath; @@ -70,20 +88,35 @@ export function createProgramProxy( } return {}; } + function getScriptVersion(fileName: string) { + return getScript(fileName)?.version ?? ''; + } function getScriptSnapshot(fileName: string) { + return getScript(fileName)?.scriptSnapshot; + } + function getScript(fileName: string) { + const script = scripts.get(fileName); - if (script) { - return script.scriptSnapshot; + if (script?.projectVersion === projectVersion) { + return script; } + + const modifiedTime = ts.sys.getModifiedTime?.(fileName)?.valueOf() ?? 0; + if (script?.modifiedTime === modifiedTime) { + return script; + } + if (host.fileExists(fileName)) { const fileContent = host.readFile(fileName); if (fileContent !== undefined) { - const scriptSnapshot = ts.ScriptSnapshot.fromString(fileContent); - scripts.set(fileName, { - scriptSnapshot: scriptSnapshot, - version: ts.sys.createHash?.(fileContent) ?? fileContent, - }); - return scriptSnapshot; + const script = { + projectVersion, + modifiedTime, + scriptSnapshot: ts.ScriptSnapshot.fromString(fileContent), + version: host.createHash?.(fileContent) ?? fileContent, + }; + scripts.set(fileName, script); + return script; } } } diff --git a/packages/vue-typescript/src/typescriptRuntime.ts b/packages/vue-typescript/src/typescriptRuntime.ts index f749a4fe1..7d2d556ed 100644 --- a/packages/vue-typescript/src/typescriptRuntime.ts +++ b/packages/vue-typescript/src/typescriptRuntime.ts @@ -151,8 +151,7 @@ export function createTypeScriptRuntime(options: { const scriptSnapshots = new Map(); const fileVersions = new WeakMap(); - const tsHost: ts.LanguageServiceHost = { - ...options.vueLsHost, + const _tsHost: Partial = { fileExists: options.vueLsHost.fileExists ? fileName => { // .vue.js -> .vue @@ -210,12 +209,18 @@ export function createTypeScriptRuntime(options: { }; if (lsType === 'template') { - tsHost.getCompilationSettings = () => ({ + _tsHost.getCompilationSettings = () => ({ ...options.vueLsHost.getCompilationSettings(), jsx: ts.JsxEmit.Preserve, }); } + const tsHost = new Proxy(_tsHost as ts.LanguageServiceHost, { + get: (target, property: keyof ts.LanguageServiceHost) => { + return target[property] || options.vueLsHost[property]; + }, + }); + return tsHost; function getScriptFileNames() {