import { type FSWatcher } from 'chokidar'; import type { EcoPagesAppConfig, IHmrManager, IClientBridge } from '../types/internal-types.js'; /** * Configuration options for the ProjectWatcher * @interface ProjectWatcherConfig * @property {EcoPagesAppConfig} config - The application configuration * @property {() => Promise} refreshRouterRoutesCallback - Callback to refresh router routes * @property {IHmrManager} hmrManager - The HMR manager instance * @property {ClientBridge} bridge - The client bridge instance */ export interface ProjectWatcherConfig { config: EcoPagesAppConfig; refreshRouterRoutesCallback: () => Promise; hmrManager: IHmrManager; bridge: IClientBridge; /** When true, the host dev server owns browser dev-client bootstrap. */ hostOwnsDevClient?: boolean; /** Delay before a change event is processed; 0 disables debouncing. */ changeDebounceMs?: number; } /** * ProjectWatcher handles file system changes for hot module replacement (HMR). * It uses chokidar to watch for file changes and triggers appropriate actions: * - Uncaches modules when files change * - Refreshes router routes for page files * - Triggers HMR server reload * - Handles processor-specific file changes * * The watcher uses chokidar's built-in debouncing through `awaitWriteFinish` * to handle rapid file changes efficiently: * - stabilityThreshold: 50ms - Time to wait for writes to stabilize * - pollInterval: 50ms - Interval to poll for file changes * * @class ProjectWatcher */ export declare class ProjectWatcher { /** * Duplicate identical watcher events within this window are ignored. * * Some editors or save pipelines emit two near-identical filesystem change * notifications for the same file. Ecopages should treat those as one logical * update so HMR and route refresh work are not repeated unnecessarily. */ private static readonly duplicateChangeWindowMs; private appConfig; private refreshRouterRoutesCallback; private hmrManager; private bridge; private readonly hostOwnsDevClient; private readonly invalidationService; private readonly changeDebounceMs; private watcher; private closed; private pendingChangeEvents; private changeQueue; constructor({ config, refreshRouterRoutesCallback, hmrManager, bridge, hostOwnsDevClient, changeDebounceMs, }: ProjectWatcherConfig); /** * Uncaches modules in the source directory to ensure fresh imports. * This is necessary for hot module replacement to work correctly. * @private */ private uncacheModules; private isRouteSourceFile; private isIncludeSourceFile; private requestBrowserReload; /** * Handles public directory file changes by copying only the changed file. * @param filePath - Absolute path of the changed file */ private handlePublicDirFileChange; /** * Serializes file change handling so that concurrent chokidar events are * processed one at a time, preventing overlapping builds and race conditions. */ private enqueueChange; /** * Handles file changes by uncaching modules, refreshing routes, and delegating appropriately. * Follows 5-rule priority: * 0. Public directory match? -> copy file and reload * 1. additionalWatchPaths match? -> reload * 2. Include template source? -> current-page refresh via HMR after processor notifications are deferred * 3. Processor-owned asset? -> processor already handled it via notification, skip HMR * 4. Otherwise -> HMR strategies * * Processors that watch a file extension as a dependency (e.g. PostCSS watching * .tsx for Tailwind class scanning) are always notified first, but do not * prevent the file from flowing through the normal HMR strategy pipeline. * * Duplicate identical watcher events for the same file are coalesced within a * short window before any of the priority rules run. * @param rawPath - Path of the changed file * @param event - The type of file system event */ private handleFileChange; private processFileChange; /** * Re-imports server modules before HMR broadcast so reload/refetch does not * race stale in-memory imports or custom-element registry state. */ private prewarmBeforeHmr; private prewarmServerModuleImports; /** * Notifies all processors whose watch config matches the given file extension. * This is called before checking processor ownership so that dependency-only * processors (e.g. PostCSS watching .tsx for class scanning) receive their * notifications regardless of whether they own the file. */ private notifyProcessors; private getProcessorHandler; /** * Checks if a file is in the public directory. */ private isPublicDirFile; /** * Checks if file path matches any additionalWatchPaths patterns. */ private matchesAdditionalWatchPaths; /** * Checks if a file is owned by a processor as an asset input. * Ownership requires declared asset capabilities; watch config only drives * {@link notifyProcessors} notifications. */ private isHandledByProcessor; /** * Triggers router refresh for page directory changes. * This ensures the router is updated when pages are added or removed. * * @param {string} path - Path of the changed directory */ triggerRouterRefresh(changedPath: string): Promise; /** * Handles and logs errors that occur during file watching. * * @param {unknown} error - The error to handle */ handleError(error: unknown): void; /** * Creates and configures the file system watcher. * This sets up: * 1. Page file watching * 2. Directory watching * 3. Error handling * * Processor notifications are dispatched inside handleFileChange, ensuring * a single unified event pipeline with no parallel chokidar bindings. * * Uses chokidar's built-in debouncing through `awaitWriteFinish` to handle * rapid file changes efficiently. */ createWatcherSubscription(): Promise; /** * Closes the active filesystem watcher subscription. * * @remarks * Safe to call multiple times. Used when tearing down dev servers in tests * and other short-lived Ecopages runtimes. */ close(): Promise; }