Skip to main content
Glama
nrwl

Nx MCP Server

Official
by nrwl
native-watcher.ts5.01 kB
import type { WatchEvent, Watcher } from 'nx/src/native'; import { importNxPackagePath } from '@nx-console/shared-npm'; import { normalize } from 'path'; import { match as minimatch } from 'minimatch'; import { Logger } from '@nx-console/shared-utils'; const NX_PLUGIN_PATTERNS_TO_WATCH = [ '**/cypress.config.{js,ts,mjs,cjs}', '**/{detox.config,.detoxrc}.{json,js}', '**/app.{json,config.js}', '**/jest.config.{cjs,mjs,js,cts,mts,ts}', '**/next.config.{js,cjs,mjs}', '**/nuxt.config.{js,ts,mjs,mts,cjs,cts}', '**/playwright.config.{js,ts,cjs,cts,mjs,mts}', '**/remix.config.{js,cjs,mjs}', '**/.storybook/main.{js,ts,mjs,mts,cjs,cts}', '**/{vite,vitest}.config.{js,ts,mjs,mts,cjs,cts}', '**/webpack.config.{js,ts,mjs,cjs}', '**/jest.preset.js', '**/tsconfig.*.json', // nx-dotnet '*{.csproj,fsproj,vbproj}', ]; export class NativeWatcher { private watcher: Watcher | undefined; private stopped = false; private debounceTimer: NodeJS.Timeout | null = null; private pendingChanges: Set<string> = new Set(); constructor( private workspacePath: string, private callback: () => unknown, private logger: Logger, ) { this.initWatcher(); } async stop() { this.stopped = true; if (this.debounceTimer) { clearTimeout(this.debounceTimer); this.debounceTimer = null; } if (this.watcher) { try { await this.watcher.stop(); } catch (e) { // Ignore errors when stopping the watcher, as it might already be closed this.logger.log( 'Error stopping watcher (this is expected during shutdown): ' + e, ); } this.watcher = undefined; } } private async initWatcher() { const native = await importNxPackagePath<typeof import('nx/src/native')>( this.workspacePath, 'src/native/index.js', this.logger, ); this.watcher = new native.Watcher(this.workspacePath); this.watcher.watch((err: string | null, events: WatchEvent[]) => { // Check if watcher is stopped before processing any events if (this.stopped) { return; } if (err) { this.logger.log('Error watching files: ' + err); } else { const relevantEvents = events.filter((e) => { const path = normalize(e.path); return ( (path.endsWith('project.json') || path.endsWith('package.json') || path.endsWith('nx.json') || path.endsWith('workspace.json') || path.endsWith('tsconfig.base.json') || NX_PLUGIN_PATTERNS_TO_WATCH.some((pattern) => minimatch([path], pattern, { dot: true }), ) || NativeWatcher.openDocuments.has(path)) && !path.startsWith('node_modules') && !path.startsWith(normalize('.nx/cache')) && !path.startsWith(normalize('.yarn/cache')) && !path.startsWith(normalize('.nx/workspace-data')) ); }); if (relevantEvents.length > 0) { // Double-check stopped state before handling changes if (this.stopped) { return; } this.handleFileChanges(relevantEvents); } } }); } private handleFileChanges(events: WatchEvent[]) { // Check if watcher is stopped before processing file changes if (this.stopped) { return; } const criticalFiles: string[] = []; const nonCriticalFiles: string[] = []; for (const event of events) { const path = normalize(event.path); const isCritical = path.endsWith('project.json') || path.endsWith('package.json') || path.endsWith('nx.json'); if (isCritical) { criticalFiles.push(path); } else { nonCriticalFiles.push(path); this.pendingChanges.add(path); } } if (criticalFiles.length > 0) { this.logger.log(`Critical files changed (triggering immediately)`); if (this.debounceTimer) { clearTimeout(this.debounceTimer); this.debounceTimer = null; } this.pendingChanges.clear(); // Check again before invoking callback if (!this.stopped) { this.callback(); } } else if (nonCriticalFiles.length > 0) { this.logger.log(`Non-critical files changed (debouncing for 10s)`); if (this.debounceTimer) { clearTimeout(this.debounceTimer); } this.debounceTimer = setTimeout(() => { if (this.pendingChanges.size > 0 && !this.stopped) { this.logger.log(`Debounce timer expired, triggering callback`); this.pendingChanges.clear(); this.callback(); } this.debounceTimer = null; }, 10000); } } private static openDocuments: Set<string> = new Set(); static onOpenDocument(uri: string) { this.openDocuments.add(normalize(uri)); } static onCloseDocument(uri: string) { this.openDocuments.delete(normalize(uri)); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/nrwl/nx-console'

If you have feedback or need assistance with the MCP directory API, please join our Discord server