Skip to main content
Glama
watcher.ts5.55 kB
import { readFile } from 'node:fs/promises'; import { basename } from 'node:path'; import { type GetConfigurationOptions, getAppLogger, getConfiguration, } from '@intlayer/config'; import type { IntlayerConfig } from '@intlayer/types'; /** @ts-ignore remove error Module '"chokidar"' has no exported member 'ChokidarOptions'. */ import { type ChokidarOptions, watch as chokidarWatch } from 'chokidar'; import { handleAdditionalContentDeclarationFile } from './handleAdditionalContentDeclarationFile'; import { handleContentDeclarationFileChange } from './handleContentDeclarationFileChange'; import { handleContentDeclarationFileMoved } from './handleContentDeclarationFileMoved'; import { handleUnlinkedContentDeclarationFile } from './handleUnlinkedContentDeclarationFile'; import { prepareIntlayer } from './prepareIntlayer'; import { writeContentDeclaration } from './writeContentDeclaration'; // Map to track files that were recently unlinked: oldPath -> { timer, timestamp } const pendingUnlinks = new Map< string, { timer: NodeJS.Timeout; oldPath: string } >(); type WatchOptions = ChokidarOptions & { configuration?: IntlayerConfig; configOptions?: GetConfigurationOptions; skipPrepare?: boolean; }; // Initialize chokidar watcher (non-persistent) export const watch = (options?: WatchOptions) => { const configuration: IntlayerConfig = options?.configuration ?? getConfiguration(options?.configOptions); const appLogger = getAppLogger(configuration); const { watch: isWatchMode, watchedFilesPatternWithPath, fileExtensions, } = configuration.content; return chokidarWatch(watchedFilesPatternWithPath, { persistent: isWatchMode, // Make the watcher persistent ignoreInitial: true, // Process existing files awaitWriteFinish: { stabilityThreshold: 1000, pollInterval: 100, }, ignored: [ '**/node_modules/**', '**/dist/**', '**/build/**', '**/.intlayer/**', ], ...options, }) .on('add', async (filePath) => { const fileName = basename(filePath); let isMove = false; // 1. Check if this Add corresponds to a pending Unlink (Move/Rename detection) // Heuristic: // - Priority A: Exact basename match (Moved to different folder) // - Priority B: Single entry in pendingUnlinks (Renamed file) let matchedOldPath: string | undefined; // Search for basename match for (const [oldPath] of pendingUnlinks) { if (basename(oldPath) === fileName) { matchedOldPath = oldPath; break; } } // If no basename match, but exactly one file was recently unlinked, assume it's a rename if (!matchedOldPath && pendingUnlinks.size === 1) { matchedOldPath = pendingUnlinks.keys().next().value; } if (matchedOldPath) { // It is a move! Cancel the unlink handler const pending = pendingUnlinks.get(matchedOldPath); if (pending) { clearTimeout(pending.timer); pendingUnlinks.delete(matchedOldPath); } isMove = true; appLogger(`File moved from ${matchedOldPath} to ${filePath}`); await handleContentDeclarationFileMoved( matchedOldPath, filePath, configuration ); } // 2. If it's NOT a move, perform standard "New File" logic if (!isMove) { const fileContent = await readFile(filePath, 'utf-8'); const isEmpty = fileContent === ''; // Fill template content declaration file if it is empty if (isEmpty) { const extensionPattern = fileExtensions .map((ext) => ext.replace(/\./g, '\\.')) .join('|'); const name = fileName.replace( new RegExp(`(${extensionPattern})$`), '' ); await writeContentDeclaration( { key: name, content: {}, filePath, }, configuration ); } } // 3. Always ensure the file is processed (both for moves and adds) await handleAdditionalContentDeclarationFile(filePath, configuration); }) .on( 'change', async (filePath) => await handleContentDeclarationFileChange(filePath, configuration) ) .on('unlink', async (filePath) => { // Delay unlink processing to see if an 'add' event occurs (indicating a move) const timer = setTimeout(async () => { // If timer fires, the file was genuinely removed pendingUnlinks.delete(filePath); await handleUnlinkedContentDeclarationFile(filePath, configuration); }, 200); // 200ms window to catch the 'add' event pendingUnlinks.set(filePath, { timer, oldPath: filePath }); }) .on('error', async (error) => { appLogger(`Watcher error: ${error}`, { level: 'error', }); appLogger('Restarting watcher'); await prepareIntlayer(configuration); }); }; export const buildAndWatchIntlayer = async (options?: WatchOptions) => { const { skipPrepare, ...rest } = options ?? {}; const configuration = options?.configuration ?? getConfiguration(options?.configOptions); if (!skipPrepare) { await prepareIntlayer(configuration, { forceRun: true }); } if (configuration.content.watch || options?.persistent) { const appLogger = getAppLogger(configuration); appLogger('Watching Intlayer content declarations'); watch({ ...rest, configuration }); } };

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/aymericzip/intlayer'

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