Skip to content

fix: respect tsconfig/jsconfig exclude patterns in file watcher #2807

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ interface LSAndTSDocResolverOptions {
tsSystem?: ts.System;
watchDirectory?: (patterns: RelativePattern[]) => void;
nonRecursiveWatchPattern?: string;
/**
* Optional callback invoked when a new snapshot is created.
* Used by svelte-check to dynamically add parent directories to file watchers.
*/
onSnapshotCreated?: (dirPath: string) => void;
}

export class LSAndTSDocResolver {
Expand Down Expand Up @@ -83,6 +88,20 @@ export class LSAndTSDocResolver {

this.tsSystem = this.wrapWithPackageJsonMonitoring(this.options?.tsSystem ?? ts.sys);
this.globalSnapshotsManager = new GlobalSnapshotsManager(this.tsSystem);
// Notify when new snapshots are created so external watchers (svelte-check)
// can add their parent directories dynamically.
if (this.options?.onSnapshotCreated) {
this.globalSnapshotsManager.onChange((fileName, newDocument) => {
if (newDocument) {
try {
const dir = dirname(fileName);
this.options?.onSnapshotCreated?.(dir);
} catch {
// best-effort; ignore errors in callback
}
}
});
}
this.userPreferencesAccessor = { preferences: this.getTsUserPreferences() };
const projectService = createProjectService(this.tsSystem, this.userPreferencesAccessor);

Expand Down
6 changes: 6 additions & 0 deletions packages/language-server/src/plugins/typescript/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export interface LanguageServiceContainer {
getResolvedProjectReferences(): TsConfigInfo[];
openVirtualDocument(document: Document): void;
isShimFiles(filePath: string): boolean;
getProjectConfig(): ts.ParsedCommandLine;
dispose(): void;
}

Expand Down Expand Up @@ -458,6 +459,7 @@ async function createLanguageService(
getResolvedProjectReferences,
openVirtualDocument,
isShimFiles,
getProjectConfig,
dispose
};

Expand Down Expand Up @@ -1249,6 +1251,10 @@ async function createLanguageService(
function isShimFiles(filePath: string) {
return svelteTsxFilesToOriginalCasing.has(getCanonicalFileName(normalizePath(filePath)));
}

function getProjectConfig() {
return projectConfig;
}
}

/**
Expand Down
29 changes: 28 additions & 1 deletion packages/language-server/src/svelte-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ export interface SvelteCheckOptions {
tsconfig?: string;
onProjectReload?: () => void;
watch?: boolean;
/**
* Optional callback invoked when a new snapshot is created.
* Used by svelte-check to dynamically add watch directories.
*/
onSnapshotCreated?: (dirPath: string) => void;
}

/**
Expand Down Expand Up @@ -91,7 +96,8 @@ export class SvelteCheck {
tsconfigPath: options.tsconfig,
isSvelteCheck: true,
onProjectReloaded: options.onProjectReload,
watch: options.watch
watch: options.watch,
onSnapshotCreated: options.onSnapshotCreated
}
);
this.pluginHost.register(
Expand Down Expand Up @@ -353,4 +359,25 @@ export class SvelteCheck {
}
return this.lsAndTSDocResolver.getTSService(tsconfigPath);
}

/**
* Gets the watch directories based on the tsconfig include patterns.
* Returns null if no tsconfig is specified.
*/
async getWatchDirectories(): Promise<{ path: string; recursive: boolean }[] | null> {
if (!this.options.tsconfig) {
return null;
}
const lsContainer = await this.getLSContainer(this.options.tsconfig);
const projectConfig = lsContainer.getProjectConfig();

if (!projectConfig.wildcardDirectories) {
return null;
}

return Object.entries(projectConfig.wildcardDirectories).map(([dir, flags]) => ({
path: dir,
recursive: !!(flags & ts.WatchDirectoryFlags.Recursive)
}));
}
}
89 changes: 73 additions & 16 deletions packages/svelte-check/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* This code's groundwork is taken from https://github.com/vuejs/vetur/tree/master/vti
*/

import { watch } from 'chokidar';
import { watch, FSWatcher } from 'chokidar';
import * as fs from 'fs';
import { fdir } from 'fdir';
import * as path from 'path';
Expand Down Expand Up @@ -143,49 +143,93 @@ async function getDiagnostics(
}
}

const FILE_ENDING_REGEX = /\.(svelte|d\.ts|ts|js|jsx|tsx|mjs|cjs|mts|cts)$/;
const VITE_CONFIG_REGEX = /vite\.config\.(js|ts)\.timestamp-/;

class DiagnosticsWatcher {
private updateDiagnostics: any;
private watcher: FSWatcher;
private currentWatchedDirs = new Set<string>();
private userIgnored: Array<(path: string) => boolean>;
private pendingWatcherUpdate: any;

constructor(
private workspaceUri: URI,
private svelteCheck: SvelteCheck,
private writer: Writer,
filePathsToIgnore: string[],
ignoreInitialAdd: boolean
private ignoreInitialAdd: boolean
) {
const fileEnding = /\.(svelte|d\.ts|ts|js|jsx|tsx|mjs|cjs|mts|cts)$/;
const viteConfigRegex = /vite\.config\.(js|ts)\.timestamp-/;
const userIgnored = createIgnored(filePathsToIgnore);
const offset = workspaceUri.fsPath.length + 1;
this.userIgnored = createIgnored(filePathsToIgnore);

watch(workspaceUri.fsPath, {
// Create watcher with initial paths
this.watcher = watch([], {
ignored: (path, stats) => {
if (
path.includes('node_modules') ||
path.includes('.git') ||
(stats?.isFile() && (!fileEnding.test(path) || viteConfigRegex.test(path)))
(stats?.isFile() &&
(!FILE_ENDING_REGEX.test(path) || VITE_CONFIG_REGEX.test(path)))
) {
return true;
}

if (userIgnored.length !== 0) {
path = path.slice(offset);
for (const i of userIgnored) {
if (i(path)) {
if (this.userIgnored.length !== 0) {
// Make path relative to workspace for user ignores
const workspaceRelative = path.startsWith(this.workspaceUri.fsPath)
? path.slice(this.workspaceUri.fsPath.length + 1)
: path;
for (const i of this.userIgnored) {
if (i(workspaceRelative)) {
return true;
}
}
}

return false;
},
ignoreInitial: ignoreInitialAdd
ignoreInitial: this.ignoreInitialAdd
})
.on('add', (path) => this.updateDocument(path, true))
.on('unlink', (path) => this.removeDocument(path))
.on('change', (path) => this.updateDocument(path, false));

if (ignoreInitialAdd) {
this.updateWatchedDirectories();
}

addWatchDirectory(dir: string) {
if (!dir || this.currentWatchedDirs.has(dir)) {
return;
}
this.watcher.add(dir);
this.currentWatchedDirs.add(dir);
// New files might now be visible; schedule a run
this.scheduleDiagnostics();
}

private async updateWatchedDirectories() {
const watchDirs = await this.svelteCheck.getWatchDirectories();
const dirsToWatch = watchDirs || [{ path: this.workspaceUri.fsPath, recursive: true }];
const newDirs = new Set(dirsToWatch.map((d) => d.path));

// Fast diff: find directories to add and remove
const toAdd = [...newDirs].filter((dir) => !this.currentWatchedDirs.has(dir));
const toRemove = [...this.currentWatchedDirs].filter((dir) => !newDirs.has(dir));

// Add new directories
if (toAdd.length > 0) {
this.watcher.add(toAdd);
}

// Remove old directories
if (toRemove.length > 0) {
this.watcher.unwatch(toRemove);
}

// Update current set
this.currentWatchedDirs = newDirs;

if (this.ignoreInitialAdd) {
this.scheduleDiagnostics();
}
}
Expand All @@ -210,6 +254,11 @@ class DiagnosticsWatcher {
this.scheduleDiagnostics();
}

updateWatchers() {
clearTimeout(this.pendingWatcherUpdate);
this.pendingWatcherUpdate = setTimeout(() => this.updateWatchedDirectories(), 1000);
}

scheduleDiagnostics() {
clearTimeout(this.updateDiagnostics);
this.updateDiagnostics = setTimeout(
Expand Down Expand Up @@ -264,8 +313,16 @@ parseOptions(async (opts) => {
};

if (opts.watch) {
svelteCheckOptions.onProjectReload = () => watcher.scheduleDiagnostics();
const watcher = new DiagnosticsWatcher(
// Wire callbacks that can reference the watcher instance created below
let watcher: DiagnosticsWatcher;
svelteCheckOptions.onProjectReload = () => {
watcher.updateWatchers();
watcher.scheduleDiagnostics();
};
svelteCheckOptions.onSnapshotCreated = (dirPath: string) => {
watcher.addWatchDirectory(dirPath);
};
watcher = new DiagnosticsWatcher(
opts.workspaceUri,
new SvelteCheck(opts.workspaceUri.fsPath, svelteCheckOptions),
writer,
Expand Down