@applicvision/js-toolbox
Version:
A collection of tools for modern JavaScript development
103 lines (92 loc) • 2.56 kB
JavaScript
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);
}
}