UNPKG

react-saasify-chrisvxd

Version:

React components for Saasify web clients.

276 lines (235 loc) 6.53 kB
const fork = require('child_process').fork; const optionsTransfer = require('./options'); const Path = require('path'); const {EventEmitter} = require('events'); const {errorUtils} = require('@parcel/utils'); /** * This watcher wraps chokidar so that we watch directories rather than individual files on macOS. * This prevents us from hitting EMFILE errors when running out of file descriptors. * Chokidar does not have support for watching directories on non-macOS platforms, so we disable * this behavior in order to prevent watching more individual files than necessary (e.g. node_modules). */ class Watcher extends EventEmitter { constructor( options = { // FS events on macOS are flakey in the tests, which write lots of files very quickly // See https://github.com/paulmillr/chokidar/issues/612 useFsEvents: process.platform === 'darwin' && process.env.NODE_ENV !== 'test', ignoreInitial: true, ignorePermissionErrors: true, ignored: /(^|[/\\])\.(git|cache)/ } ) { super(); this.options = optionsTransfer.encode(options); this.watchedPaths = new Set(); this.child = null; this.ready = false; this.readyQueue = []; this.watchedDirectories = new Map(); this.stopped = false; this.on('ready', () => { this.ready = true; for (let func of this.readyQueue) { func(); } this.readyQueue = []; }); this.startchild(); } startchild() { if (this.child) return; let filteredArgs = process.execArgv.filter( v => !/^--(debug|inspect)/.test(v) ); let options = { execArgv: filteredArgs, env: process.env, cwd: process.cwd() }; this.child = fork(Path.join(__dirname, 'child'), process.argv, options); if (this.watchedPaths.size > 0) { this.sendCommand('add', [Array.from(this.watchedPaths)]); } this.child.send({ type: 'init', options: this.options }); this.child.on('message', msg => this.handleEmit(msg.event, msg.path)); this.child.on('error', () => {}); this.child.on('exit', () => this.handleClosed()); // this.child.on('close', () => this.handleClosed()); } handleClosed() { if (!this.stopped) { // Restart the child this.child = null; this.ready = false; this.startchild(); } this.emit('childDead'); } handleEmit(event, data) { if (event === 'watcherError') { data = errorUtils.jsonToError(data); } this.emit(event, data); } sendCommand(func, args) { if (!this.ready) { return this.readyQueue.push(() => this.sendCommand(func, args)); } this.child.send({ type: 'function', name: func, args: args }); } _addPath(path) { if (!this.watchedPaths.has(path)) { this.watchedPaths.add(path); return true; } } add(paths) { let added = false; if (Array.isArray(paths)) { for (let path of paths) { added = !added ? this._addPath(path) : true; } } else { added = this._addPath(paths); } if (added) this.sendCommand('add', [paths]); } _closePath(path) { if (this.watchedPaths.has(path)) { this.watchedPaths.delete(path); } this.sendCommand('_closePath', [path]); } _emulateChildDead() { if (!this.child) { return; } this.child.send({ type: 'die' }); } _emulateChildError() { if (!this.child) { return; } this.child.send({ type: 'emulate_error' }); } getWatched() { let watchList = {}; for (let path of this.watchedPaths) { let key = this.options.cwd ? Path.relative(this.options.cwd, path) : path; watchList[key || '.'] = []; } return watchList; } /** * Find a parent directory of `path` which is already watched */ getWatchedParent(path) { path = Path.dirname(path); let root = Path.parse(path).root; while (path !== root) { if (this.watchedDirectories.has(path)) { return path; } path = Path.dirname(path); } return null; } /** * Find a list of child directories of `path` which are already watched */ getWatchedChildren(path) { path = Path.dirname(path) + Path.sep; let res = []; for (let dir of this.watchedDirectories.keys()) { if (dir.startsWith(path)) { res.push(dir); } } return res; } /** * Add a path to the watcher */ watch(path) { if (this.shouldWatchDirs) { // If there is no parent directory already watching this path, add a new watcher. let parent = this.getWatchedParent(path); if (!parent) { // Find watchers on child directories, and remove them. They will be handled by the new parent watcher. let children = this.getWatchedChildren(path); let count = 1; for (let dir of children) { count += this.watchedDirectories.get(dir); this._closePath(dir); this.watchedDirectories.delete(dir); } let dir = Path.dirname(path); this.add(dir); this.watchedDirectories.set(dir, count); } else { // Otherwise, increment the reference count of the parent watcher. this.watchedDirectories.set( parent, this.watchedDirectories.get(parent) + 1 ); } } else { this.add(path); } } _unwatch(paths) { let removed = false; if (Array.isArray(paths)) { for (let p of paths) { removed = !removed ? this.watchedPaths.delete(p) : true; } } else { removed = this.watchedPaths.delete(paths); } if (removed) this.sendCommand('unwatch', [paths]); } /** * Remove a path from the watcher */ unwatch(path) { if (this.shouldWatchDirs) { let dir = this.getWatchedParent(path); if (dir) { // When the count of files watching a directory reaches zero, unwatch it. let count = this.watchedDirectories.get(dir) - 1; if (count === 0) { this.watchedDirectories.delete(dir); this._unwatch(dir); } else { this.watchedDirectories.set(dir, count); } } } else { this._unwatch(path); } } /** * Stop watching all paths */ async stop() { this.stopped = true; if (this.child) { this.child.kill(); return new Promise(resolve => this.once('childDead', resolve)); } } } module.exports = Watcher;