diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts index cd2a19996..f926289ae 100644 --- a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts +++ b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts @@ -13,23 +13,28 @@ 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; + watchTsConfig?: boolean; +} + +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 @@ -65,15 +70,20 @@ export class LSAndTSDocResolver { }; private globalSnapshotsManager = new GlobalSnapshotsManager(); + private extendedConfigCache = new Map(); 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 + notifyExceedSizeLimit: this.options?.notifyExceedSizeLimit, + extendedConfigCache: this.extendedConfigCache, + onProjectReloaded: this.options?.onProjectReloaded, + watchTsConfig: !!this.options?.watchTsConfig, + tsSystem: ts.sys }; } @@ -157,8 +167,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/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..2ad397c22 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -37,11 +37,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 +66,10 @@ export interface LanguageServiceDocumentContext { createDocument: (fileName: string, content: string) => Document; globalSnapshotsManager: GlobalSnapshotsManager; notifyExceedSizeLimit: (() => void) | undefined; + extendedConfigCache: Map; + onProjectReloaded: (() => void) | undefined; + watchTsConfig: boolean; + tsSystem: ts.System; } export async function getService( @@ -67,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); } @@ -91,7 +101,15 @@ 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; @@ -105,8 +123,14 @@ async function createLanguageService( docContext: LanguageServiceDocumentContext ): Promise { const workspacePath = tsconfigPath ? dirname(tsconfigPath) : ''; + const { tsSystem } = docContext; - 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( @@ -135,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; @@ -158,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); @@ -172,11 +196,14 @@ async function createLanguageService( typingsNamespace: raw?.svelteOptions?.namespace || 'svelteHTML' }; - docContext.globalSnapshotsManager.onChange(() => { + const onSnapshotChange = () => { projectVersion++; - }); + }; + docContext.globalSnapshotsManager.onChange(onSnapshotChange); reduceLanguageServiceCapabilityIfFileSizeTooBig(); + updateExtendedConfigDependents(); + watchConfigFile(); return { tsconfigPath, @@ -188,7 +215,8 @@ async function createLanguageService( updateTsOrJsFile, hasFile, fileBelongsToProject, - snapshotManager + snapshotManager, + dispose }; function deleteSnapshot(filePath: string): void { @@ -304,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 @@ -317,9 +345,27 @@ 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, + tsSystem, workspacePath, forcedCompilerOptions, tsconfigPath, @@ -335,7 +381,8 @@ async function createLanguageService( ts.ScriptKind.Deferred ?? (docContext.useNewTransformation ? ts.ScriptKind.TS : ts.ScriptKind.TSX) } - ] + ], + cacheMonitorProxy ); const compilerOptions: ts.CompilerOptions = { @@ -391,7 +438,8 @@ async function createLanguageService( return { ...parsedConfig, fileNames: parsedConfig.fileNames.map(normalizePath), - options: compilerOptions + options: compilerOptions, + extendedConfigPaths }; } @@ -423,12 +471,72 @@ 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?.(); } } + + function dispose() { + languageService.dispose(); + snapshotManager.dispose(); + configWatchers.get(tsconfigPath)?.close(); + configWatchers.delete(tsconfigPath); + 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 (!tsSystem.watchFile || !docContext.watchTsConfig) { + return; + } + + if (!configWatchers.has(tsconfigPath) && tsconfigPath) { + configWatchers.set(tsconfigPath, tsSystem.watchFile(tsconfigPath, watchConfigCallback)); + } + + for (const config of extendedConfigPaths) { + if (extendedConfigWatchers.has(config)) { + continue; + } + + extendedConfigWatchers.set( + config, + tsSystem.watchFile(config, createWatchExtendedConfigCallback(docContext)) + ); + } + } + + async function watchConfigCallback(fileName: string, kind: ts.FileWatcherEventKind) { + dispose(); + + if (kind === ts.FileWatcherEventKind.Changed) { + scheduleReload(fileName); + } else if (kind === ts.FileWatcherEventKind.Deleted) { + services.delete(fileName); + } + + docContext.onProjectReloaded?.(); + } } /** @@ -437,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; @@ -458,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); @@ -481,3 +590,36 @@ 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 async (fileName: string) => { + docContext.extendedConfigCache.delete(fileName); + + 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?.(); + }; +} + +/** + * 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/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/src/server.ts b/packages/language-server/src/server.ts index 690906a4f..ff225d9ba 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -161,12 +161,11 @@ 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, + watchTsConfig: true + }) ) ); diff --git a/packages/language-server/src/svelte-check.ts b/packages/language-server/src/svelte-check.ts index 502988cf6..e72a4e636 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; } /** @@ -83,9 +85,12 @@ export class SvelteCheck { this.docManager, [pathToUrl(workspacePath)], this.configManager, - undefined, - true, - options.tsconfig + { + tsconfigPath: options.tsconfig, + isSvelteCheck: true, + onProjectReloaded: options.onProjectReload, + watchTsConfig: options.watch + } ); this.pluginHost.register( new TypeScriptPlugin(this.configManager, this.lsAndTSDocResolver) 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' + ); + } + }); +}); 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); } 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$/,