From 5216b9f59c3054eb67ace360c992c4d6fcbf1ddf Mon Sep 17 00:00:00 2001 From: Lyu Jason Date: Tue, 21 Jun 2022 22:52:06 +0800 Subject: [PATCH 1/7] first crack --- .../plugins/typescript/LSAndTSDocResolver.ts | 4 +- .../src/plugins/typescript/MapCacheMonitor.ts | 0 .../src/plugins/typescript/SnapshotManager.ts | 39 +++-- .../src/plugins/typescript/service.ts | 138 +++++++++++++++++- packages/svelte-vscode/src/extension.ts | 4 +- 5 files changed, 161 insertions(+), 24 deletions(-) create mode 100644 packages/language-server/src/plugins/typescript/MapCacheMonitor.ts diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts index cd2a19996..e1f75d2df 100644 --- a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts +++ b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts @@ -65,6 +65,7 @@ export class LSAndTSDocResolver { }; private globalSnapshotsManager = new GlobalSnapshotsManager(); + private extendedConfigCache = new Map(); private get lsDocumentContext(): LanguageServiceDocumentContext { return { @@ -73,7 +74,8 @@ export class LSAndTSDocResolver { useNewTransformation: this.configManager.getConfig().svelte.useNewTransformation, transformOnTemplateError: !this.isSvelteCheck, globalSnapshotsManager: this.globalSnapshotsManager, - notifyExceedSizeLimit: this.notifyExceedSizeLimit + notifyExceedSizeLimit: this.notifyExceedSizeLimit, + extendedConfigCache: this.extendedConfigCache }; } diff --git a/packages/language-server/src/plugins/typescript/MapCacheMonitor.ts b/packages/language-server/src/plugins/typescript/MapCacheMonitor.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/language-server/src/plugins/typescript/SnapshotManager.ts b/packages/language-server/src/plugins/typescript/SnapshotManager.ts index 4b69643bc..23c73c404 100644 --- a/packages/language-server/src/plugins/typescript/SnapshotManager.ts +++ b/packages/language-server/src/plugins/typescript/SnapshotManager.ts @@ -5,6 +5,8 @@ import { TextDocumentContentChangeEvent } from 'vscode-languageserver'; import { normalizePath } from '../../utils'; import { EventEmitter } from 'events'; +type SnapshotChangeHandler = (fileName: string, newDocument: DocumentSnapshot | undefined) => void; + /** * Every snapshot corresponds to a unique file on disk. * A snapshot can be part of multiple projects, but for a given file path @@ -60,9 +62,13 @@ export class GlobalSnapshotsManager { } } - onChange(listener: (fileName: string, newDocument: DocumentSnapshot | undefined) => void) { + onChange(listener: SnapshotChangeHandler) { this.emitter.on('change', listener); } + + removeChangeListener(listener: SnapshotChangeHandler) { + this.emitter.off('change', listener); + } } export interface TsFilesSpec { @@ -92,18 +98,21 @@ export class SnapshotManager { private fileSpec: TsFilesSpec, private workspaceRoot: string ) { - this.globalSnapshotsManager.onChange((fileName, document) => { - // Only delete/update snapshots, don't add new ones, - // as they could be from another TS service and this - // snapshot manager can't reach this file. - // For these, instead wait on a `get` method invocation - // and set them "manually" in the set/update methods. - if (!document) { - this.documents.delete(fileName); - } else if (this.documents.has(fileName)) { - this.documents.set(fileName, document); - } - }); + this.onSnapshotChange = this.onSnapshotChange.bind(this); + this.globalSnapshotsManager.onChange(this.onSnapshotChange); + } + + private onSnapshotChange(fileName: string, document: DocumentSnapshot | undefined) { + // Only delete/update snapshots, don't add new ones, + // as they could be from another TS service and this + // snapshot manager can't reach this file. + // For these, instead wait on a `get` method invocation + // and set them "manually" in the set/update methods. + if (!document) { + this.documents.delete(fileName); + } else if (this.documents.has(fileName)) { + this.documents.set(fileName, document); + } } updateProjectFiles(): void { @@ -191,6 +200,10 @@ export class SnapshotManager { ); } } + + dispose() { + this.globalSnapshotsManager.removeChangeListener(this.onSnapshotChange); + } } export const ignoredBuildDirectories = ['__sapper__', '.svelte-kit']; diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 91c180ebc..8d64ce64e 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -18,6 +18,7 @@ import { ensureRealSvelteFilePath, findTsConfigPath, hasTsExtensions } from './u export interface LanguageServiceContainer { readonly tsconfigPath: string; readonly compilerOptions: ts.CompilerOptions; + readonly extendedConfigPaths: Set; /** * @internal Public for tests only */ @@ -37,11 +38,17 @@ export interface LanguageServiceContainer { * Only works for TS versions that have ScriptKind.Deferred */ fileBelongsToProject(filePath: string): boolean; + + dispose(): void; } const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024; // 20 MB const services = new Map>(); -const serviceSizeMap: Map = new Map(); +const serviceSizeMap = new Map(); +const configWatchers = new Map(); +const extendedConfigWatchers = new Map(); +const extendedConfigToTsConfigPath = new Map>(); +const pendingReloads = new Set(); /** * For testing only: Reset the cache for services. @@ -60,6 +67,7 @@ export interface LanguageServiceDocumentContext { createDocument: (fileName: string, content: string) => Document; globalSnapshotsManager: GlobalSnapshotsManager; notifyExceedSizeLimit: (() => void) | undefined; + extendedConfigCache: Map; } export async function getService( @@ -91,22 +99,107 @@ export async function getServiceForTsconfig( if (services.has(tsconfigPath)) { service = await services.get(tsconfigPath)!; } else { - Logger.log('Initialize new ts service at ', tsconfigPath); + const reloading = pendingReloads.has(tsconfigPath); + + if (reloading) { + Logger.log('Reloading ts service at ', tsconfigPath, 'due to config updated'); + } else { + Logger.log('Initialize new ts service at ', tsconfigPath); + } + + pendingReloads.delete(tsconfigPath); const newService = createLanguageService(tsconfigPath, docContext); services.set(tsconfigPath, newService); service = await newService; + + updateExtendedConfigDependents(service); + watchConfigFile(service, docContext); } return service; } +function updateExtendedConfigDependents(service: LanguageServiceContainer) { + service.extendedConfigPaths.forEach((extendedConfig) => { + let dependedTsConfig = extendedConfigToTsConfigPath.get(extendedConfig); + if (!dependedTsConfig) { + dependedTsConfig = new Set(); + extendedConfigToTsConfigPath.set(extendedConfig, dependedTsConfig); + } + + dependedTsConfig.add(service.tsconfigPath); + }); +} + +function watchConfigFile(ls: LanguageServiceContainer, docContext: LanguageServiceDocumentContext) { + if (!ts.sys.watchFile) { + return; + } + + if (!configWatchers.has(ls.tsconfigPath) && ls.tsconfigPath) { + configWatchers.set( + ls.tsconfigPath, + ts.sys.watchFile(ls.tsconfigPath, (filename, kind) => + watchConfigCallback(filename, kind, docContext) + ) + ); + } + + for (const config of ls.extendedConfigPaths) { + if (extendedConfigWatchers.has(config)) { + continue; + } + extendedConfigWatchers.set( + config, + ts.sys.watchFile(config, (filename, kind) => + watchExtendedConfigCallback(filename, kind, docContext) + ) + ); + } +} + +async function watchExtendedConfigCallback( + extendedConfig: string, + kind: ts.FileWatcherEventKind, + docContext: LanguageServiceDocumentContext +) { + docContext.extendedConfigCache.delete(extendedConfig); + + extendedConfigToTsConfigPath.get(extendedConfig)?.forEach((config) => { + services.delete(config); + pendingReloads.add(config); + }); +} + +async function watchConfigCallback( + fileName: string, + kind: ts.FileWatcherEventKind, + docContext: LanguageServiceDocumentContext +) { + const oldService = services.get(fileName); + services.delete(fileName); + docContext.extendedConfigCache.delete(fileName); + (await oldService)?.dispose(); + pendingReloads.add(fileName); + + if (kind === ts.FileWatcherEventKind.Deleted && !extendedConfigToTsConfigPath.has(fileName)) { + configWatchers.get(fileName)?.close(); + configWatchers.delete(fileName); + } +} + async function createLanguageService( tsconfigPath: string, docContext: LanguageServiceDocumentContext ): Promise { const workspacePath = tsconfigPath ? dirname(tsconfigPath) : ''; - const { options: compilerOptions, fileNames: files, raw } = getParsedConfig(); + const { + options: compilerOptions, + fileNames: files, + raw, + extendedConfigPaths + } = getParsedConfig(); // raw is the tsconfig merged with extending config // see: https://github.com/microsoft/TypeScript/blob/08e4f369fbb2a5f0c30dee973618d65e6f7f09f8/src/compiler/commandLineParser.ts#L2537 const snapshotManager = new SnapshotManager( @@ -172,9 +265,10 @@ async function createLanguageService( typingsNamespace: raw?.svelteOptions?.namespace || 'svelteHTML' }; - docContext.globalSnapshotsManager.onChange(() => { + const onSnapshotChange = () => { projectVersion++; - }); + }; + docContext.globalSnapshotsManager.onChange(onSnapshotChange); reduceLanguageServiceCapabilityIfFileSizeTooBig(); @@ -188,7 +282,9 @@ async function createLanguageService( updateTsOrJsFile, hasFile, fileBelongsToProject, - snapshotManager + snapshotManager, + extendedConfigPaths, + dispose }; function deleteSnapshot(filePath: string): void { @@ -317,6 +413,24 @@ async function createLanguageService( ); } + const extendedConfigPaths = new Set(); + const { extendedConfigCache } = docContext; + const cacheMonitorProxy = { + ...docContext.extendedConfigCache, + get(key: string) { + extendedConfigPaths.add(key); + return extendedConfigCache.get(key); + }, + has(key: string) { + extendedConfigPaths.add(key); + return extendedConfigCache.has(key); + }, + set(key: string, value: ts.ExtendedConfigCacheEntry) { + extendedConfigPaths.add(key); + return extendedConfigCache.set(key, value); + } + }; + const parsedConfig = ts.parseJsonConfigFileContent( configJson, ts.sys, @@ -335,7 +449,8 @@ async function createLanguageService( ts.ScriptKind.Deferred ?? (docContext.useNewTransformation ? ts.ScriptKind.TS : ts.ScriptKind.TSX) } - ] + ], + cacheMonitorProxy ); const compilerOptions: ts.CompilerOptions = { @@ -391,7 +506,8 @@ async function createLanguageService( return { ...parsedConfig, fileNames: parsedConfig.fileNames.map(normalizePath), - options: compilerOptions + options: compilerOptions, + extendedConfigPaths }; } @@ -429,6 +545,12 @@ async function createLanguageService( docContext.notifyExceedSizeLimit?.(); } } + + function dispose() { + languageService.dispose(); + snapshotManager.dispose(); + docContext.globalSnapshotsManager.removeChangeListener(onSnapshotChange); + } } /** diff --git a/packages/svelte-vscode/src/extension.ts b/packages/svelte-vscode/src/extension.ts index f7df8427c..af62a8375 100644 --- a/packages/svelte-vscode/src/extension.ts +++ b/packages/svelte-vscode/src/extension.ts @@ -182,8 +182,8 @@ export function activateSvelteLanguageServer(context: ExtensionContext) { const parts = doc.uri.toString(true).split(/\/|\\/); if ( [ - /^tsconfig\.json$/, - /^jsconfig\.json$/, + // /^tsconfig\.json$/, + // /^jsconfig\.json$/, /^svelte\.config\.(js|cjs|mjs)$/, // https://prettier.io/docs/en/configuration.html /^\.prettierrc$/, From 1bae9e5e9128bafb177a2c8c2de945863a4113b5 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Wed, 22 Jun 2022 13:11:44 +0800 Subject: [PATCH 2/7] update diagnotics when reloaded --- .../plugins/typescript/LSAndTSDocResolver.ts | 39 +++++++++++-------- .../src/plugins/typescript/service.ts | 1 + packages/language-server/src/server.ts | 10 ++--- packages/language-server/src/svelte-check.ts | 7 ++-- 4 files changed, 31 insertions(+), 26 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts index e1f75d2df..56adef29c 100644 --- a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts +++ b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts @@ -13,23 +13,27 @@ import { } from './service'; import { GlobalSnapshotsManager, SnapshotManager } from './SnapshotManager'; -export class LSAndTSDocResolver { +interface LSAndTSDocResolverOptions { + notifyExceedSizeLimit?: () => void; + /** + * True, if used in the context of svelte-check + */ + isSvelteCheck?: boolean; + /** - * - * @param docManager - * @param workspaceUris - * @param configManager - * @param notifyExceedSizeLimit - * @param isSvelteCheck True, if used in the context of svelte-check - * @param tsconfigPath This should only be set via svelte-check. Makes sure all documents are resolved to that tsconfig. Has to be absolute. + * This should only be set via svelte-check. Makes sure all documents are resolved to that tsconfig. Has to be absolute. */ + tsconfigPath?: string; + + onProjectReloaded?: () => void; +} + +export class LSAndTSDocResolver { constructor( private readonly docManager: DocumentManager, private readonly workspaceUris: string[], private readonly configManager: LSConfigManager, - private readonly notifyExceedSizeLimit?: () => void, - private readonly isSvelteCheck = false, - private readonly tsconfigPath?: string + private readonly options?: LSAndTSDocResolverOptions ) { const handleDocumentChange = (document: Document) => { // This refreshes the document in the ts language service @@ -69,13 +73,14 @@ export class LSAndTSDocResolver { private get lsDocumentContext(): LanguageServiceDocumentContext { return { - ambientTypesSource: this.isSvelteCheck ? 'svelte-check' : 'svelte2tsx', + ambientTypesSource: this.options?.isSvelteCheck ? 'svelte-check' : 'svelte2tsx', createDocument: this.createDocument, useNewTransformation: this.configManager.getConfig().svelte.useNewTransformation, - transformOnTemplateError: !this.isSvelteCheck, + transformOnTemplateError: !this.options?.isSvelteCheck, globalSnapshotsManager: this.globalSnapshotsManager, - notifyExceedSizeLimit: this.notifyExceedSizeLimit, - extendedConfigCache: this.extendedConfigCache + notifyExceedSizeLimit: this.options?.notifyExceedSizeLimit, + extendedConfigCache: this.extendedConfigCache, + onProjectReloaded: this.options?.onProjectReloaded }; } @@ -159,8 +164,8 @@ export class LSAndTSDocResolver { } async getTSService(filePath?: string): Promise { - if (this.tsconfigPath) { - return getServiceForTsconfig(this.tsconfigPath, this.lsDocumentContext); + if (this.options?.tsconfigPath) { + return getServiceForTsconfig(this.options?.tsconfigPath, this.lsDocumentContext); } if (!filePath) { throw new Error('Cannot call getTSService without filePath and without tsconfigPath'); diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 8d64ce64e..7576d3e29 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -68,6 +68,7 @@ export interface LanguageServiceDocumentContext { globalSnapshotsManager: GlobalSnapshotsManager; notifyExceedSizeLimit: (() => void) | undefined; extendedConfigCache: Map; + onProjectReloaded: (() => void) | undefined; } export async function getService( diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index 690906a4f..8138d67bf 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -161,12 +161,10 @@ export function startServer(options?: LSOptions) { pluginHost.register( new TypeScriptPlugin( configManager, - new LSAndTSDocResolver( - docManager, - workspaceUris.map(normalizeUri), - configManager, - notifyTsServiceExceedSizeLimit - ) + new LSAndTSDocResolver(docManager, workspaceUris.map(normalizeUri), configManager, { + notifyExceedSizeLimit: notifyTsServiceExceedSizeLimit, + onProjectReloaded: updateAllDiagnostics + }) ) ); diff --git a/packages/language-server/src/svelte-check.ts b/packages/language-server/src/svelte-check.ts index 502988cf6..7b060206b 100644 --- a/packages/language-server/src/svelte-check.ts +++ b/packages/language-server/src/svelte-check.ts @@ -83,9 +83,10 @@ export class SvelteCheck { this.docManager, [pathToUrl(workspacePath)], this.configManager, - undefined, - true, - options.tsconfig + { + tsconfigPath: options.tsconfig, + isSvelteCheck: true + } ); this.pluginHost.register( new TypeScriptPlugin(this.configManager, this.lsAndTSDocResolver) From 25f4146f1b31782dd021e7cf93023a86f6ea63a3 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Wed, 22 Jun 2022 13:36:51 +0800 Subject: [PATCH 3/7] svelte check watch tsconfig --- .../plugins/typescript/LSAndTSDocResolver.ts | 4 +++- .../src/plugins/typescript/service.ts | 3 ++- packages/language-server/src/server.ts | 3 ++- packages/language-server/src/svelte-check.ts | 6 +++++- packages/svelte-check/src/index.ts | 18 +++++++++++------- 5 files changed, 23 insertions(+), 11 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts index 56adef29c..9253f25e3 100644 --- a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts +++ b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts @@ -26,6 +26,7 @@ interface LSAndTSDocResolverOptions { tsconfigPath?: string; onProjectReloaded?: () => void; + watchTsConfig?: boolean } export class LSAndTSDocResolver { @@ -80,7 +81,8 @@ export class LSAndTSDocResolver { globalSnapshotsManager: this.globalSnapshotsManager, notifyExceedSizeLimit: this.options?.notifyExceedSizeLimit, extendedConfigCache: this.extendedConfigCache, - onProjectReloaded: this.options?.onProjectReloaded + onProjectReloaded: this.options?.onProjectReloaded, + watchTsConfig: !!this.options?.watchTsConfig }; } diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 7576d3e29..4276cebea 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -69,6 +69,7 @@ export interface LanguageServiceDocumentContext { notifyExceedSizeLimit: (() => void) | undefined; extendedConfigCache: Map; onProjectReloaded: (() => void) | undefined; + watchTsConfig: boolean; } export async function getService( @@ -133,7 +134,7 @@ function updateExtendedConfigDependents(service: LanguageServiceContainer) { } function watchConfigFile(ls: LanguageServiceContainer, docContext: LanguageServiceDocumentContext) { - if (!ts.sys.watchFile) { + if (!ts.sys.watchFile || !docContext.watchTsConfig) { return; } diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index 8138d67bf..ff225d9ba 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -163,7 +163,8 @@ export function startServer(options?: LSOptions) { configManager, new LSAndTSDocResolver(docManager, workspaceUris.map(normalizeUri), configManager, { notifyExceedSizeLimit: notifyTsServiceExceedSizeLimit, - onProjectReloaded: updateAllDiagnostics + onProjectReloaded: updateAllDiagnostics, + watchTsConfig: true }) ) ); diff --git a/packages/language-server/src/svelte-check.ts b/packages/language-server/src/svelte-check.ts index 7b060206b..7884fcfa7 100644 --- a/packages/language-server/src/svelte-check.ts +++ b/packages/language-server/src/svelte-check.ts @@ -30,6 +30,8 @@ export interface SvelteCheckOptions { * Whether or not to use the new transformation of svelte2tsx */ useNewTransformation?: boolean; + onProjectReload?: () => void; + watch?: boolean } /** @@ -85,7 +87,9 @@ export class SvelteCheck { this.configManager, { tsconfigPath: options.tsconfig, - isSvelteCheck: true + isSvelteCheck: true, + onProjectReloaded: options.onProjectReload, + watchTsConfig: options.watch } ); this.pluginHost.register( diff --git a/packages/svelte-check/src/index.ts b/packages/svelte-check/src/index.ts index c7cb8e510..48039816c 100644 --- a/packages/svelte-check/src/index.ts +++ b/packages/svelte-check/src/index.ts @@ -6,7 +6,7 @@ import { watch } from 'chokidar'; import * as fs from 'fs'; import glob from 'fast-glob'; import * as path from 'path'; -import { SvelteCheck } from 'svelte-language-server'; +import { SvelteCheck, SvelteCheckOptions } from 'svelte-language-server'; import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver-protocol'; import { URI } from 'vscode-uri'; import { parseOptions, SvelteCheckCliOptions } from './options'; @@ -133,7 +133,7 @@ class DiagnosticsWatcher { this.scheduleDiagnostics(); } - private scheduleDiagnostics() { + scheduleDiagnostics() { clearTimeout(this.updateDiagnostics); this.updateDiagnostics = setTimeout( () => getDiagnostics(this.workspaceUri, this.writer, this.svelteCheck), @@ -174,22 +174,26 @@ parseOptions(async (opts) => { try { const writer = instantiateWriter(opts); - const svelteCheck = new SvelteCheck(opts.workspaceUri.fsPath, { + const svelteCheckOptions: SvelteCheckOptions = { compilerWarnings: opts.compilerWarnings, diagnosticSources: opts.diagnosticSources, tsconfig: opts.tsconfig, - useNewTransformation: opts.useNewTransformation - }); + useNewTransformation: opts.useNewTransformation, + watch: opts.watch + }; if (opts.watch) { - new DiagnosticsWatcher( + svelteCheckOptions.onProjectReload = () => watcher.scheduleDiagnostics(); + const watcher = new DiagnosticsWatcher( opts.workspaceUri, - svelteCheck, + new SvelteCheck(opts.workspaceUri.fsPath, svelteCheckOptions), writer, opts.filePathsToIgnore, !!opts.tsconfig ); } else { + const svelteCheck = new SvelteCheck(opts.workspaceUri.fsPath, svelteCheckOptions); + if (!opts.tsconfig) { await openAllDocuments(opts.workspaceUri, opts.filePathsToIgnore, svelteCheck); } From 92bbea238e3233aced25451ebec5a167e48342b8 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Wed, 22 Jun 2022 13:51:29 +0800 Subject: [PATCH 4/7] space --- packages/language-server/src/plugins/typescript/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 4276cebea..053edc60e 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -104,7 +104,7 @@ export async function getServiceForTsconfig( const reloading = pendingReloads.has(tsconfigPath); if (reloading) { - Logger.log('Reloading ts service at ', tsconfigPath, 'due to config updated'); + Logger.log('Reloading ts service at ', tsconfigPath, ' due to config updated'); } else { Logger.log('Initialize new ts service at ', tsconfigPath); } From a22e28313a45e5959c71e7afb134d356859524ef Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Wed, 22 Jun 2022 18:00:19 +0800 Subject: [PATCH 5/7] move watch functions into createLanguageService --- .../plugins/typescript/LSAndTSDocResolver.ts | 2 +- .../src/plugins/typescript/service.ts | 148 +++++++++--------- packages/language-server/src/svelte-check.ts | 2 +- 3 files changed, 76 insertions(+), 76 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts index 9253f25e3..bc79f9fcb 100644 --- a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts +++ b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts @@ -26,7 +26,7 @@ interface LSAndTSDocResolverOptions { tsconfigPath?: string; onProjectReloaded?: () => void; - watchTsConfig?: boolean + watchTsConfig?: boolean; } export class LSAndTSDocResolver { diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 053edc60e..e0769496b 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -18,7 +18,6 @@ import { ensureRealSvelteFilePath, findTsConfigPath, hasTsExtensions } from './u export interface LanguageServiceContainer { readonly tsconfigPath: string; readonly compilerOptions: ts.CompilerOptions; - readonly extendedConfigPaths: Set; /** * @internal Public for tests only */ @@ -113,83 +112,11 @@ export async function getServiceForTsconfig( const newService = createLanguageService(tsconfigPath, docContext); services.set(tsconfigPath, newService); service = await newService; - - updateExtendedConfigDependents(service); - watchConfigFile(service, docContext); } return service; } -function updateExtendedConfigDependents(service: LanguageServiceContainer) { - service.extendedConfigPaths.forEach((extendedConfig) => { - let dependedTsConfig = extendedConfigToTsConfigPath.get(extendedConfig); - if (!dependedTsConfig) { - dependedTsConfig = new Set(); - extendedConfigToTsConfigPath.set(extendedConfig, dependedTsConfig); - } - - dependedTsConfig.add(service.tsconfigPath); - }); -} - -function watchConfigFile(ls: LanguageServiceContainer, docContext: LanguageServiceDocumentContext) { - if (!ts.sys.watchFile || !docContext.watchTsConfig) { - return; - } - - if (!configWatchers.has(ls.tsconfigPath) && ls.tsconfigPath) { - configWatchers.set( - ls.tsconfigPath, - ts.sys.watchFile(ls.tsconfigPath, (filename, kind) => - watchConfigCallback(filename, kind, docContext) - ) - ); - } - - for (const config of ls.extendedConfigPaths) { - if (extendedConfigWatchers.has(config)) { - continue; - } - extendedConfigWatchers.set( - config, - ts.sys.watchFile(config, (filename, kind) => - watchExtendedConfigCallback(filename, kind, docContext) - ) - ); - } -} - -async function watchExtendedConfigCallback( - extendedConfig: string, - kind: ts.FileWatcherEventKind, - docContext: LanguageServiceDocumentContext -) { - docContext.extendedConfigCache.delete(extendedConfig); - - extendedConfigToTsConfigPath.get(extendedConfig)?.forEach((config) => { - services.delete(config); - pendingReloads.add(config); - }); -} - -async function watchConfigCallback( - fileName: string, - kind: ts.FileWatcherEventKind, - docContext: LanguageServiceDocumentContext -) { - const oldService = services.get(fileName); - services.delete(fileName); - docContext.extendedConfigCache.delete(fileName); - (await oldService)?.dispose(); - pendingReloads.add(fileName); - - if (kind === ts.FileWatcherEventKind.Deleted && !extendedConfigToTsConfigPath.has(fileName)) { - configWatchers.get(fileName)?.close(); - configWatchers.delete(fileName); - } -} - async function createLanguageService( tsconfigPath: string, docContext: LanguageServiceDocumentContext @@ -273,6 +200,8 @@ async function createLanguageService( docContext.globalSnapshotsManager.onChange(onSnapshotChange); reduceLanguageServiceCapabilityIfFileSizeTooBig(); + updateExtendedConfigDependents(); + watchConfigFile(); return { tsconfigPath, @@ -285,7 +214,6 @@ async function createLanguageService( hasFile, fileBelongsToProject, snapshotManager, - extendedConfigPaths, dispose }; @@ -553,6 +481,78 @@ async function createLanguageService( snapshotManager.dispose(); docContext.globalSnapshotsManager.removeChangeListener(onSnapshotChange); } + + function updateExtendedConfigDependents() { + extendedConfigPaths.forEach((extendedConfig) => { + let dependedTsConfig = extendedConfigToTsConfigPath.get(extendedConfig); + if (!dependedTsConfig) { + dependedTsConfig = new Set(); + extendedConfigToTsConfigPath.set(extendedConfig, dependedTsConfig); + } + + dependedTsConfig.add(tsconfigPath); + }); + } + + function watchConfigFile() { + if (!ts.sys.watchFile || !docContext.watchTsConfig) { + return; + } + + if (!configWatchers.has(tsconfigPath) && tsconfigPath) { + configWatchers.set(tsconfigPath, ts.sys.watchFile(tsconfigPath, watchConfigCallback)); + } + + for (const config of extendedConfigPaths) { + if (extendedConfigWatchers.has(config)) { + continue; + } + + extendedConfigWatchers.set( + config, + ts.sys.watchFile(config, (filename) => + watchExtendedConfigCallback(filename, docContext) + ) + ); + } + } + + async function watchExtendedConfigCallback( + extendedConfig: string, + docContext: LanguageServiceDocumentContext + ) { + docContext.extendedConfigCache.delete(extendedConfig); + + extendedConfigToTsConfigPath.get(extendedConfig)?.forEach(async (config) => { + const oldService = services.get(config); + scheduleReload(config); + (await oldService)?.dispose(); + }); + } + + async function watchConfigCallback(fileName: string, kind: ts.FileWatcherEventKind) { + dispose(); + scheduleReload(fileName); + + if ( + kind === ts.FileWatcherEventKind.Deleted && + !extendedConfigToTsConfigPath.has(fileName) + ) { + configWatchers.get(fileName)?.close(); + configWatchers.delete(fileName); + } + } + + /** + * schedule to the service reload to the next time the + * service in requested + * if there's still files opened it should be restarted + * in the onProjectReloaded hooks + */ + function scheduleReload(fileName: string) { + services.delete(fileName); + pendingReloads.add(fileName); + } } /** diff --git a/packages/language-server/src/svelte-check.ts b/packages/language-server/src/svelte-check.ts index 7884fcfa7..e72a4e636 100644 --- a/packages/language-server/src/svelte-check.ts +++ b/packages/language-server/src/svelte-check.ts @@ -31,7 +31,7 @@ export interface SvelteCheckOptions { */ useNewTransformation?: boolean; onProjectReload?: () => void; - watch?: boolean + watch?: boolean; } /** From eb1fd34aba66f4a1ceff944a6d1612584810ea37 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Thu, 23 Jun 2022 14:30:15 +0800 Subject: [PATCH 6/7] move the watch callback out reusing watcher would cause memory leak --- .../src/plugins/typescript/service.ts | 69 ++++++++++--------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index e0769496b..f6b32f381 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -479,6 +479,8 @@ async function createLanguageService( function dispose() { languageService.dispose(); snapshotManager.dispose(); + configWatchers.get(tsconfigPath)?.close(); + configWatchers.delete(tsconfigPath); docContext.globalSnapshotsManager.removeChangeListener(onSnapshotChange); } @@ -510,49 +512,20 @@ async function createLanguageService( extendedConfigWatchers.set( config, - ts.sys.watchFile(config, (filename) => - watchExtendedConfigCallback(filename, docContext) - ) + ts.sys.watchFile(config, createWatchExtendedConfigCallback(docContext)) ); } } - async function watchExtendedConfigCallback( - extendedConfig: string, - docContext: LanguageServiceDocumentContext - ) { - docContext.extendedConfigCache.delete(extendedConfig); - - extendedConfigToTsConfigPath.get(extendedConfig)?.forEach(async (config) => { - const oldService = services.get(config); - scheduleReload(config); - (await oldService)?.dispose(); - }); - } - async function watchConfigCallback(fileName: string, kind: ts.FileWatcherEventKind) { dispose(); - scheduleReload(fileName); - if ( - kind === ts.FileWatcherEventKind.Deleted && - !extendedConfigToTsConfigPath.has(fileName) - ) { - configWatchers.get(fileName)?.close(); - configWatchers.delete(fileName); + if (kind === ts.FileWatcherEventKind.Changed) { + scheduleReload(fileName); + } else if (kind === ts.FileWatcherEventKind.Deleted) { + services.delete(fileName); } } - - /** - * schedule to the service reload to the next time the - * service in requested - * if there's still files opened it should be restarted - * in the onProjectReloaded hooks - */ - function scheduleReload(fileName: string) { - services.delete(fileName); - pendingReloads.add(fileName); - } } /** @@ -605,3 +578,31 @@ function exceedsTotalSizeLimitForNonTsFiles( serviceSizeMap.set(tsconfigPath, totalNonTsFileSize); return false; } + +/** + * shared watcher callback can't be within `createLanguageService` + * because it would reference the closure + * So that GC won't drop it and cause memory leaks + */ +function createWatchExtendedConfigCallback(docContext: LanguageServiceDocumentContext) { + return (fileName: string) => { + docContext.extendedConfigCache.delete(fileName); + + extendedConfigToTsConfigPath.get(fileName)?.forEach(async (config) => { + const oldService = services.get(config); + scheduleReload(config); + (await oldService)?.dispose(); + }); + }; +} + +/** + * schedule to the service reload to the next time the + * service in requested + * if there's still files opened it should be restarted + * in the onProjectReloaded hooks + */ +function scheduleReload(fileName: string) { + services.delete(fileName); + pendingReloads.add(fileName); +} From 361ec4ec9e9a199f56196840bcac91340e097505 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Sun, 26 Jun 2022 12:44:59 +0800 Subject: [PATCH 7/7] test and actually call the onProjectReloaded hook --- .../plugins/typescript/LSAndTSDocResolver.ts | 3 +- .../src/plugins/typescript/MapCacheMonitor.ts | 0 .../src/plugins/typescript/service.ts | 57 ++-- .../src/plugins/typescript/utils.ts | 10 +- .../test/plugins/typescript/service.test.ts | 272 ++++++++++++++++++ 5 files changed, 318 insertions(+), 24 deletions(-) delete mode 100644 packages/language-server/src/plugins/typescript/MapCacheMonitor.ts create mode 100644 packages/language-server/test/plugins/typescript/service.test.ts diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts index bc79f9fcb..f926289ae 100644 --- a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts +++ b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts @@ -82,7 +82,8 @@ export class LSAndTSDocResolver { notifyExceedSizeLimit: this.options?.notifyExceedSizeLimit, extendedConfigCache: this.extendedConfigCache, onProjectReloaded: this.options?.onProjectReloaded, - watchTsConfig: !!this.options?.watchTsConfig + watchTsConfig: !!this.options?.watchTsConfig, + tsSystem: ts.sys }; } diff --git a/packages/language-server/src/plugins/typescript/MapCacheMonitor.ts b/packages/language-server/src/plugins/typescript/MapCacheMonitor.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index f6b32f381..2ad397c22 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -69,6 +69,7 @@ export interface LanguageServiceDocumentContext { extendedConfigCache: Map; onProjectReloaded: (() => void) | undefined; watchTsConfig: boolean; + tsSystem: ts.System; } export async function getService( @@ -76,7 +77,7 @@ export async function getService( workspaceUris: string[], docContext: LanguageServiceDocumentContext ): Promise { - const tsconfigPath = findTsConfigPath(path, workspaceUris); + const tsconfigPath = findTsConfigPath(path, workspaceUris, docContext.tsSystem.fileExists); return getServiceForTsconfig(tsconfigPath, docContext); } @@ -122,6 +123,7 @@ async function createLanguageService( docContext: LanguageServiceDocumentContext ): Promise { const workspacePath = tsconfigPath ? dirname(tsconfigPath) : ''; + const { tsSystem } = docContext; const { options: compilerOptions, @@ -157,7 +159,7 @@ async function createLanguageService( './svelte-shims.d.ts', './svelte-jsx.d.ts', './svelte-native-jsx.d.ts' - ].map((f) => ts.sys.resolvePath(resolve(svelteTsPath, f))); + ].map((f) => tsSystem.resolvePath(resolve(svelteTsPath, f))); let languageServiceReducedMode = false; let projectVersion = 0; @@ -180,11 +182,11 @@ async function createLanguageService( readFile: svelteModuleLoader.readFile, resolveModuleNames: svelteModuleLoader.resolveModuleNames, readDirectory: svelteModuleLoader.readDirectory, - getDirectories: ts.sys.getDirectories, - useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames, + getDirectories: tsSystem.getDirectories, + useCaseSensitiveFileNames: () => tsSystem.useCaseSensitiveFileNames, getScriptKind: (fileName: string) => getSnapshot(fileName).scriptKind, getProjectVersion: () => projectVersion.toString(), - getNewLine: () => ts.sys.newLine + getNewLine: () => tsSystem.newLine }; let languageService = ts.createLanguageService(host); @@ -330,7 +332,7 @@ async function createLanguageService( // always let ts parse config to get default compilerOption let configJson = - (tsconfigPath && ts.readConfigFile(tsconfigPath, ts.sys.readFile).config) || + (tsconfigPath && ts.readConfigFile(tsconfigPath, tsSystem.readFile).config) || getDefaultJsConfig(); // Only default exclude when no extends for now @@ -363,7 +365,7 @@ async function createLanguageService( const parsedConfig = ts.parseJsonConfigFileContent( configJson, - ts.sys, + tsSystem, workspacePath, forcedCompilerOptions, tsconfigPath, @@ -469,7 +471,14 @@ async function createLanguageService( * large projects with improperly excluded tsconfig. */ function reduceLanguageServiceCapabilityIfFileSizeTooBig() { - if (exceedsTotalSizeLimitForNonTsFiles(compilerOptions, tsconfigPath, snapshotManager)) { + if ( + exceedsTotalSizeLimitForNonTsFiles( + compilerOptions, + tsconfigPath, + snapshotManager, + tsSystem + ) + ) { languageService.cleanupSemanticCache(); languageServiceReducedMode = true; docContext.notifyExceedSizeLimit?.(); @@ -497,12 +506,12 @@ async function createLanguageService( } function watchConfigFile() { - if (!ts.sys.watchFile || !docContext.watchTsConfig) { + if (!tsSystem.watchFile || !docContext.watchTsConfig) { return; } if (!configWatchers.has(tsconfigPath) && tsconfigPath) { - configWatchers.set(tsconfigPath, ts.sys.watchFile(tsconfigPath, watchConfigCallback)); + configWatchers.set(tsconfigPath, tsSystem.watchFile(tsconfigPath, watchConfigCallback)); } for (const config of extendedConfigPaths) { @@ -512,7 +521,7 @@ async function createLanguageService( extendedConfigWatchers.set( config, - ts.sys.watchFile(config, createWatchExtendedConfigCallback(docContext)) + tsSystem.watchFile(config, createWatchExtendedConfigCallback(docContext)) ); } } @@ -525,6 +534,8 @@ async function createLanguageService( } else if (kind === ts.FileWatcherEventKind.Deleted) { services.delete(fileName); } + + docContext.onProjectReloaded?.(); } } @@ -534,7 +545,8 @@ async function createLanguageService( function exceedsTotalSizeLimitForNonTsFiles( compilerOptions: ts.CompilerOptions, tsconfigPath: string, - snapshotManager: SnapshotManager + snapshotManager: SnapshotManager, + tsSystem: ts.System ): boolean { if (compilerOptions.disableSizeLimit) { return false; @@ -555,12 +567,12 @@ function exceedsTotalSizeLimitForNonTsFiles( continue; } - totalNonTsFileSize += ts.sys.getFileSize?.(fileName) ?? 0; + totalNonTsFileSize += tsSystem.getFileSize?.(fileName) ?? 0; if (totalNonTsFileSize > availableSpace) { const top5LargestFiles = fileNames .filter((name) => !hasTsExtensions(name)) - .map((name) => ({ name, size: ts.sys.getFileSize?.(name) ?? 0 })) + .map((name) => ({ name, size: tsSystem.getFileSize?.(name) ?? 0 })) .sort((a, b) => b.size - a.size) .slice(0, 5); @@ -585,14 +597,19 @@ function exceedsTotalSizeLimitForNonTsFiles( * So that GC won't drop it and cause memory leaks */ function createWatchExtendedConfigCallback(docContext: LanguageServiceDocumentContext) { - return (fileName: string) => { + return async (fileName: string) => { docContext.extendedConfigCache.delete(fileName); - extendedConfigToTsConfigPath.get(fileName)?.forEach(async (config) => { - const oldService = services.get(config); - scheduleReload(config); - (await oldService)?.dispose(); - }); + const promises = Array.from(extendedConfigToTsConfigPath.get(fileName) ?? []).map( + async (config) => { + const oldService = services.get(config); + scheduleReload(config); + (await oldService)?.dispose(); + } + ); + + await Promise.all(promises); + docContext.onProjectReloaded?.(); }; } diff --git a/packages/language-server/src/plugins/typescript/utils.ts b/packages/language-server/src/plugins/typescript/utils.ts index 2e04817ff..7291c753a 100644 --- a/packages/language-server/src/plugins/typescript/utils.ts +++ b/packages/language-server/src/plugins/typescript/utils.ts @@ -124,12 +124,16 @@ export function rangeToTextSpan( return { start, length: end - start }; } -export function findTsConfigPath(fileName: string, rootUris: string[]) { +export function findTsConfigPath( + fileName: string, + rootUris: string[], + fileExists: (path: string) => boolean +) { const searchDir = dirname(fileName); const path = - ts.findConfigFile(searchDir, ts.sys.fileExists, 'tsconfig.json') || - ts.findConfigFile(searchDir, ts.sys.fileExists, 'jsconfig.json') || + ts.findConfigFile(searchDir, fileExists, 'tsconfig.json') || + ts.findConfigFile(searchDir, fileExists, 'jsconfig.json') || ''; // Don't return config files that exceed the current workspace context. return !!path && rootUris.some((rootUri) => isSubPath(rootUri, path)) ? path : ''; diff --git a/packages/language-server/test/plugins/typescript/service.test.ts b/packages/language-server/test/plugins/typescript/service.test.ts new file mode 100644 index 000000000..b6d20534e --- /dev/null +++ b/packages/language-server/test/plugins/typescript/service.test.ts @@ -0,0 +1,272 @@ +import path from 'path'; +import assert from 'assert'; +import ts, { FileWatcherEventKind } from 'typescript'; +import { Document } from '../../../src/lib/documents'; +import { + getService, + LanguageServiceDocumentContext +} from '../../../src/plugins/typescript/service'; +import { GlobalSnapshotsManager } from '../../../src/plugins/typescript/SnapshotManager'; +import { normalizePath, pathToUrl } from '../../../src/utils'; + +describe('service', () => { + const testDir = path.join(__dirname, 'testfiles'); + + function setup() { + const virtualFs = new Map(); + // array behave more similar to the actual fs event than Set + const watchers = new Map(); + const watchTimeout = new Map>>(); + + const virtualSystem: ts.System = { + ...ts.sys, + writeFile(path, data) { + const normalizedPath = normalizePath(path); + const existsBefore = virtualFs.has(normalizedPath); + virtualFs.set(normalizedPath, data); + triggerWatch( + normalizedPath, + existsBefore ? ts.FileWatcherEventKind.Changed : ts.FileWatcherEventKind.Created + ); + }, + readFile(path) { + return virtualFs.get(normalizePath(path)); + }, + fileExists(path) { + return virtualFs.has(normalizePath(path)); + }, + deleteFile(path) { + const normalizedPath = normalizePath(path); + const existsBefore = virtualFs.has(normalizedPath); + virtualFs.delete(normalizedPath); + + if (existsBefore) { + triggerWatch(normalizedPath, ts.FileWatcherEventKind.Deleted); + } + }, + watchFile(path, callback) { + const normalizedPath = normalizePath(path); + let watchersOfPath = watchers.get(normalizedPath); + + if (!watchersOfPath) { + watchersOfPath = []; + watchers.set(normalizedPath, watchersOfPath); + } + + watchersOfPath.push(callback); + + return { + close() { + const watchersOfPath = watchers.get(normalizedPath); + + if (watchersOfPath) { + watchers.set( + normalizedPath, + watchersOfPath.filter((watcher) => watcher === callback) + ); + } + + const timeouts = watchTimeout.get(normalizedPath); + + if (timeouts != null) { + timeouts.forEach((timeout) => clearTimeout(timeout)); + } + } + }; + } + }; + + const lsDocumentContext: LanguageServiceDocumentContext = { + ambientTypesSource: 'svelte2tsx', + createDocument(fileName, content) { + return new Document(pathToUrl(fileName), content); + }, + extendedConfigCache: new Map(), + globalSnapshotsManager: new GlobalSnapshotsManager(), + transformOnTemplateError: true, + tsSystem: virtualSystem, + useNewTransformation: true, + watchTsConfig: false, + notifyExceedSizeLimit: undefined, + onProjectReloaded: undefined + }; + + const rootUris = [pathToUrl(testDir)]; + + return { virtualSystem, lsDocumentContext, rootUris }; + + function triggerWatch(normalizedPath: string, kind: FileWatcherEventKind) { + let timeoutsOfPath = watchTimeout.get(normalizedPath); + + if (!timeoutsOfPath) { + timeoutsOfPath = []; + watchTimeout.set(normalizedPath, timeoutsOfPath); + } + + timeoutsOfPath.push( + setTimeout( + () => + watchers + .get(normalizedPath) + ?.forEach((callback) => callback(normalizedPath, kind)), + 0 + ) + ); + } + } + + function getRandomVirtualDirPath() { + return path.join(testDir, `virtual-path-${Math.floor(Math.random() * 100_000)}`); + } + + it('can find tsconfig and override with default config', async () => { + const dirPath = getRandomVirtualDirPath(); + const { virtualSystem, lsDocumentContext, rootUris } = setup(); + + virtualSystem.writeFile( + path.join(dirPath, 'tsconfig.json'), + JSON.stringify({ + compilerOptions: { + checkJs: true, + strict: true + } + }) + ); + + const ls = await getService( + path.join(dirPath, 'random.svelte'), + rootUris, + lsDocumentContext + ); + + // ts internal + delete ls.compilerOptions.configFilePath; + + assert.deepStrictEqual(ls.compilerOptions, { + allowJs: true, + allowNonTsExtensions: true, + checkJs: true, + strict: true, + declaration: false, + module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.NodeJs, + noEmit: true, + skipLibCheck: true, + target: ts.ScriptTarget.ESNext + }); + }); + + function createReloadTester( + docContext: LanguageServiceDocumentContext, + testAfterReload: () => Promise + ) { + let _resolve: () => void; + const reloadPromise = new Promise((resolve) => { + _resolve = resolve; + }); + + return { + docContextWithReload: { + ...docContext, + async onProjectReloaded() { + try { + await testAfterReload(); + } finally { + _resolve(); + } + } + }, + reloadPromise + }; + } + + it('can watch tsconfig', async () => { + const dirPath = getRandomVirtualDirPath(); + const { virtualSystem, lsDocumentContext, rootUris } = setup(); + const tsconfigPath = path.join(dirPath, 'tsconfig.json'); + + virtualSystem.writeFile( + tsconfigPath, + JSON.stringify({ + compilerOptions: { + strict: false + } + }) + ); + + const { reloadPromise, docContextWithReload } = createReloadTester( + { ...lsDocumentContext, watchTsConfig: true }, + testAfterReload + ); + + await getService(path.join(dirPath, 'random.svelte'), rootUris, docContextWithReload); + + virtualSystem.writeFile( + tsconfigPath, + JSON.stringify({ + compilerOptions: { + strict: true + } + }) + ); + + await reloadPromise; + + async function testAfterReload() { + const newLs = await getService(path.join(dirPath, 'random.svelte'), rootUris, { + ...lsDocumentContext, + watchTsConfig: true + }); + assert.strictEqual( + newLs.compilerOptions.strict, + true, + 'expected to reload compilerOptions' + ); + } + }); + + it('can watch extended tsconfig', async () => { + const dirPath = getRandomVirtualDirPath(); + const { virtualSystem, lsDocumentContext, rootUris } = setup(); + const tsconfigPath = path.join(dirPath, 'tsconfig.json'); + const extend = './.svelte-kit/tsconfig.json'; + const extendedConfigPathFull = path.resolve(tsconfigPath, extend); + + virtualSystem.writeFile( + tsconfigPath, + JSON.stringify({ + extends: extend + }) + ); + + const { reloadPromise, docContextWithReload } = createReloadTester( + { ...lsDocumentContext, watchTsConfig: true }, + testAfterReload + ); + + await getService(path.join(dirPath, 'random.svelte'), rootUris, docContextWithReload); + + virtualSystem.writeFile( + extendedConfigPathFull, + JSON.stringify({ + compilerOptions: { + strict: true + } + }) + ); + + await reloadPromise; + + async function testAfterReload() { + const newLs = await getService(path.join(dirPath, 'random.svelte'), rootUris, { + ...lsDocumentContext, + watchTsConfig: true + }); + assert.strictEqual( + newLs.compilerOptions.strict, + true, + 'expected to reload compilerOptions' + ); + } + }); +});