UNPKG

@bscotch/debounce-watch

Version:

Monitor files for changes, debounce events, and finally trigger consequences.

127 lines 4.67 kB
/** * @file General watcher utility for re-running functions after debounced * file-change events */ import { assert, explode, isArray } from '@bscotch/utility'; import chokidar from 'chokidar'; import fs from 'fs'; import path from 'path'; function createLogger(customLogger) { return { info: console.log, warn: console.log, error: console.error, // eslint-disable-next-line @typescript-eslint/no-empty-function debug: process.env.DEBUG == 'true' ? console.log : () => { }, ...customLogger, }; } const watcherEventNames = [ 'add', 'change', 'unlink', 'addDir', 'unlinkDir', ]; /** * Watch for file system events and debounce them. Collect * events during debouncing, and once events have stopped * call a target function while providing the list of events. */ export function debounceWatch( /** * Function to call after debounced change events. * Currently no arguments are passed to the function, * but that could change. */ eventProcessor, watchFolder, options) { const logger = createLogger(options?.logger); logger.info(`Running in watch mode at "${process.cwd()}"`); logger.debug(`Watching folder "${watchFolder}"`); assert(watchFolder, `Watch folder is required.`); // Set up the watcher const extensions = isArray(options?.onlyFileExtensions) ? options.onlyFileExtensions : explode(options?.onlyFileExtensions); const debounceWaitMillis = (options?.debounceWaitSeconds || 1) * 1000; let debounceTimeout = null; const pollInterval = Math.round(debounceWaitMillis / 3); const watcher = chokidar.watch(watchFolder, { // polling seems to be a lot more reliable (if also a lot less efficient) usePolling: true, interval: pollInterval, binaryInterval: pollInterval, disableGlobbing: true, ignored: (path) => { const stat = fs.existsSync(path) && fs.statSync(path); if (stat) { if (stat.isDirectory()) { return false; } const matchesExtension = !extensions[0] || extensions.some((ext) => path.endsWith(`.${ext.replace(/^\./, '')}`)); return !matchesExtension; } // Chokidar first calls with `path` only. // When this function returns `false` in that case, // Chokidar runs again with both arguments. return false; }, awaitWriteFinish: { stabilityThreshold: Math.round(pollInterval / 2), pollInterval: Math.round(pollInterval / 4), }, ...options?.chokidarWatchOptions, }); // Collect events while waiting for debouncing to stop, // then call the supplied onChange with the accumulated events. let events = []; let running = false; const debouncedRun = (event) => { logger.debug('Change detected, debouncing'); events.push(event); clearTimeout(debounceTimeout); debounceTimeout = setTimeout(async () => { // Prevent overlapping runs if (running && !options?.allowOverlappingRuns) { logger.debug('Attempted to run while already running.'); return; } logger.debug('Watcher detected changes'); running = true; const eventsCopy = [...events]; events = []; // reset to start collecting next debounced batch await eventProcessor(eventsCopy); running = false; }, debounceWaitMillis); }; // Set up the watcher watcher.on('error', async (err) => { logger.error('Closing watcher due to error...', err); await watcher.close(); process.exit(1); }); (options?.events || ['add', 'change']).forEach((event) => { watcher.on(event, (filePath, stats) => { logger.debug(`Detected event ${event} on path ${filePath}`); debouncedRun({ event, relativePath: filePath, absolutePath: path.resolve(watchFolder, filePath), parsedPath: path.parse(path.resolve(watchFolder, filePath)), stats, }); }); }); // Don't need to call the function right out of the gate, // because the watcher triggers 'add' events when it loads. return new Promise((resolve) => { watcher.on('ready', () => resolve(watcher)); }); } /** * @alias debounceWatch * @deprecated */ export const runAfterDebouncedFileSystemEvents = debounceWatch; //# sourceMappingURL=runner.js.map