diff --git a/.changeset/green-adults-hammer.md b/.changeset/green-adults-hammer.md new file mode 100644 index 000000000..ed327704f --- /dev/null +++ b/.changeset/green-adults-hammer.md @@ -0,0 +1,5 @@ +--- +'svelte-check': patch +--- + +fix: prevent file watcher issue diff --git a/.changeset/thirty-seas-post.md b/.changeset/thirty-seas-post.md new file mode 100644 index 000000000..8aadd734e --- /dev/null +++ b/.changeset/thirty-seas-post.md @@ -0,0 +1,6 @@ +--- +'svelte-language-server': patch +'svelte-check': patch +--- + +perf: check if file content changed in tsconfig file watch diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 5271a5224..0161a4628 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -965,7 +965,11 @@ async function createLanguageService( ) { if ( kind === ts.FileWatcherEventKind.Changed && - !configFileModified(fileName, modifiedTime ?? tsSystem.getModifiedTime?.(fileName)) + !configFileModified( + fileName, + modifiedTime ?? tsSystem.getModifiedTime?.(fileName), + docContext + ) ) { return; } @@ -1328,7 +1332,8 @@ function createWatchDependedConfigCallback(docContext: LanguageServiceDocumentCo kind === ts.FileWatcherEventKind.Changed && !configFileModified( fileName, - modifiedTime ?? docContext.tsSystem.getModifiedTime?.(fileName) + modifiedTime ?? docContext.tsSystem.getModifiedTime?.(fileName), + docContext ) ) { return; @@ -1360,7 +1365,11 @@ function createWatchDependedConfigCallback(docContext: LanguageServiceDocumentCo /** * check if file content is modified instead of attributes changed */ -function configFileModified(fileName: string, modifiedTime: Date | undefined) { +function configFileModified( + fileName: string, + modifiedTime: Date | undefined, + docContext: LanguageServiceDocumentContext +) { const previousModifiedTime = configFileModifiedTime.get(fileName); if (!modifiedTime || !previousModifiedTime) { return true; @@ -1371,6 +1380,20 @@ function configFileModified(fileName: string, modifiedTime: Date | undefined) { } configFileModifiedTime.set(fileName, modifiedTime); + + const oldSourceFile = + parsedTsConfigInfo.get(fileName)?.parsedCommandLine?.options.configFile ?? + docContext.extendedConfigCache.get(fileName)?.extendedResult; + + if ( + oldSourceFile && + typeof oldSourceFile === 'object' && + 'kind' in oldSourceFile && + typeof oldSourceFile.text === 'string' && + oldSourceFile.text === docContext.tsSystem.readFile(fileName) + ) { + return false; + } return true; } diff --git a/packages/svelte-check/src/index.ts b/packages/svelte-check/src/index.ts index aaeffc422..b803050f7 100644 --- a/packages/svelte-check/src/index.ts +++ b/packages/svelte-check/src/index.ts @@ -194,13 +194,17 @@ class DiagnosticsWatcher { .on('unlink', (path) => this.removeDocument(path)) .on('change', (path) => this.updateDocument(path, false)); - this.updateWatchedDirectories(); - if (this.ignoreInitialAdd) { - getDiagnostics(this.workspaceUri, this.writer, this.svelteCheck); - } + this.updateWildcardWatcher().then(() => { + // ensuring the typescript program is built after wildcard watchers are added + // so that individual file watchers added from onFileSnapshotCreated + // run after the wildcard ones + if (this.ignoreInitialAdd) { + getDiagnostics(this.workspaceUri, this.writer, this.svelteCheck); + } + }); } - private isSubdir(candidate: string, parent: string) { + private isSubDir(candidate: string, parent: string) { const c = path.resolve(candidate); const p = path.resolve(parent); return c === p || c.startsWith(p + path.sep); @@ -210,7 +214,7 @@ class DiagnosticsWatcher { const sorted = [...new Set(dirs.map((d) => path.resolve(d)))].sort(); const result: string[] = []; for (const dir of sorted) { - if (!result.some((p) => this.isSubdir(dir, p))) { + if (!result.some((p) => this.isSubDir(dir, p))) { result.push(dir); } } @@ -218,29 +222,29 @@ class DiagnosticsWatcher { } addWatchDirectory(dir: string) { - if (!dir) return; + if (!dir) { + return; + } + // Skip if already covered by an existing watched directory for (const existing of this.currentWatchedDirs) { - if (this.isSubdir(dir, existing)) { + if (this.isSubDir(dir, existing)) { return; } } - // If new dir is a parent of existing ones, unwatch children - const toRemove: string[] = []; + + // Don't remove existing watchers, chokidar `unwatch` ignores future events from that path instead of closing the watcher in some cases for (const existing of this.currentWatchedDirs) { - if (this.isSubdir(existing, dir)) { - toRemove.push(existing); + if (this.isSubDir(existing, dir)) { + this.currentWatchedDirs.delete(existing); } } - if (toRemove.length) { - this.watcher.unwatch(toRemove); - for (const r of toRemove) this.currentWatchedDirs.delete(r); - } + this.watcher.add(dir); this.currentWatchedDirs.add(dir); } - private async updateWatchedDirectories() { + private async updateWildcardWatcher() { const watchDirs = await this.svelteCheck.getWatchDirectories(); const desired = this.minimizeDirs( (watchDirs?.map((d) => d.path) || [this.workspaceUri.fsPath]).map((p) => @@ -249,15 +253,13 @@ class DiagnosticsWatcher { ); const current = new Set([...this.currentWatchedDirs].map((p) => path.resolve(p))); - const desiredSet = new Set(desired); const toAdd = desired.filter((d) => !current.has(d)); - const toRemove = [...current].filter((d) => !desiredSet.has(d)); - - if (toAdd.length) this.watcher.add(toAdd); - if (toRemove.length) this.watcher.unwatch(toRemove); + if (toAdd.length) { + this.watcher.add(toAdd); + } - this.currentWatchedDirs = new Set(desired); + this.currentWatchedDirs = new Set([...current, ...toAdd]); } private async updateDocument(path: string, isNew: boolean) { @@ -280,9 +282,9 @@ class DiagnosticsWatcher { this.scheduleDiagnostics(); } - updateWatchers() { + updateWildcardWatchers() { clearTimeout(this.pendingWatcherUpdate); - this.pendingWatcherUpdate = setTimeout(() => this.updateWatchedDirectories(), 1000); + this.pendingWatcherUpdate = setTimeout(() => this.updateWildcardWatcher(), 1000); } scheduleDiagnostics() { @@ -342,7 +344,7 @@ parseOptions(async (opts) => { // Wire callbacks that can reference the watcher instance created below let watcher: DiagnosticsWatcher; svelteCheckOptions.onProjectReload = () => { - watcher.updateWatchers(); + watcher.updateWildcardWatchers(); watcher.scheduleDiagnostics(); }; svelteCheckOptions.onFileSnapshotCreated = (filePath: string) => {