UNPKG

@applicvision/js-toolbox

Version:

A collection of tools for modern JavaScript development

103 lines (92 loc) 2.56 kB
import stream from 'node:stream' import { stat, watch } from 'node:fs/promises' import path from 'node:path' const { Readable, Writable } = stream; import { createServer } from './server.js'; export class FileChangeStream extends Readable { abortController = new AbortController() /** * @param {string[]} watchPaths * @param {string[]} ignorePatterns * @param {stream.ReadableOptions=} options */ constructor(watchPaths, ignorePatterns, options) { super(options) this.setupListeners(watchPaths) this.ignorePatterns = ignorePatterns } async setupListeners(watchedPaths) { watchedPaths.map(async path => { const pathStats = await stat(path) const isDirectory = pathStats.isDirectory() const watcher = watch(path, { recursive: isDirectory, signal: this.abortController.signal }) try { for await (const changeEvent of watcher) { this.handleFileChange(isDirectory ? path : null, changeEvent) } } catch (error) { if (error.name == 'AbortError') { return } throw error } }) } /** * @param {string|null} directory * @param {import('node:fs/promises').FileChangeInfo} event */ handleFileChange(directory, { eventType, filename }) { if ( eventType === 'change' && !this.blockEvents && !filename.startsWith('.git') && !this.ignorePatterns.some(pattern => filename.includes(pattern)) ) { this.blockEvents = true; setTimeout(() => this.blockEvents = false, 1000); const filePath = path.relative( process.cwd(), path.resolve(directory ?? '', filename) ); if (!this.push(this.readableObjectMode ? { 'file': filePath } : `av-filechange:${filePath}\n`)) { console.log('failed to push event'); } } } _read() { } stop() { this.abortController.abort() this.push(null) } } export class ClientNotifier extends Writable { /** * @param {{port: number, certificate?: string, paths: string[], ignorePatterns: string[]}} serverOptions * @param {any=} options **/ constructor(serverOptions, options) { super(options); this.server = createServer(serverOptions) this.on('pipe', () => this.startServer(serverOptions.port)) this.on('unpipe', () => this.stopServer()) } async startServer(port) { await this.server.start(port) } stopServer() { this.server.stop() } _write(chunk, encoding, callback) { const input = chunk.toString(); const prefix = 'av-filechange:' if (input.startsWith(prefix)) { this.server.notifyClients(input.slice(prefix.length)) } process.stdout.write(chunk); callback(null); } }