UNPKG

electronmon

Version:

watch and reload your electron app the easy way

231 lines (190 loc) 5.63 kB
const path = require('path'); const { spawn } = require('child_process'); const logger = require('./log.js'); const watch = require('./watch.js'); const SIGNAL = require('./signal.js'); const ERRORED = -1; const isStdReadable = stream => stream === process.stdin; const isStdWritable = stream => stream === process.stdout || stream === process.stderr; module.exports = ({ cwd = process.cwd(), args = ['.'], env = {}, logLevel = 'info', electronPath, stdio = [process.stdin, process.stdout, process.stderr], patterns = [] } = {}) => { const executable = electronPath || require('electron'); const isTTY = stdio[1].isTTY; const getEnv = (env) => Object.assign( isTTY ? { FORCE_COLOR: '1' } : {}, process.env, { ELECTRONMON_LOGLEVEL: logLevel }, env ); const log = logger(stdio[1], logLevel); const appfiles = {}; let globalWatcher; let globalApp; let overrideSignal; function onTerm() { if (globalApp) { globalApp.kill('SIGINT'); } process.exit(0); } function onMessage({ type, file }) { if (type === 'discover') { appfiles[file] = true; } else if (type === 'uncaught-exception') { log.info('uncaught exception occured'); log.info('waiting for any change to restart the app'); overrideSignal = ERRORED; } } function startApp() { return new Promise((resolve) => { overrideSignal = null; const hook = path.resolve(__dirname, 'hook.js'); const argv = ['--require', hook].concat(args); const stdioArg = [ isStdReadable(stdio[0]) ? 'inherit' : 'pipe', isStdWritable(stdio[1]) ? 'inherit' : 'pipe', isStdWritable(stdio[2]) ? 'inherit' : 'pipe', 'ipc' ]; const app = spawn(executable, argv, { stdio: stdioArg, env: getEnv(env), cwd, windowsHide: false, }); stdioArg.forEach((val, idx) => { if (val !== 'pipe') { return; } if (idx === 0) { stdio[0].pipe(app.stdin); } else if (idx === 1) { app.stdout.pipe(stdio[1], { end: false }); } else if (idx === 2) { app.stderr.pipe(stdio[2], { end: false }); } }); app.on('message', onMessage); app.once('exit', (code, signal) => { process.removeListener('SIGTERM', onTerm); process.removeListener('SIGHUP', onTerm); globalApp = null; if (overrideSignal === ERRORED) { log.info(`ignoring exit with code ${code}`); return; } if (overrideSignal === SIGNAL || code === SIGNAL) { log.info('restarting app due to file change'); startApp(); return; } if (code) { log.info(`app exited with code ${code}, waiting for change to restart it`); } else { log.info(`app exited due to signal (${signal}), waiting for change to restart it`); } }); process.once('SIGTERM', onTerm); process.once('SIGHUP', onTerm); globalApp = app; const send = app.send.bind(app); globalApp.send = (signal) => { send(signal); if (signal === 'reset') { // app is being killed, ignore all future messages globalApp.send = () => {}; } }; resolve(app); }); } function closeApp() { return new Promise((resolve) => { if (!globalApp) { return resolve(); } globalApp.once('exit', () => { globalApp = null; resolve(); }); globalApp.kill('SIGINT'); }); } function restartApp() { return closeApp().then(() => init()); } function reloadApp() { // this is a convenience method to reload the renderer // thread in the app... also, everything is a promise if (!globalApp) { return restartApp(); } return new Promise((resolve) => { globalApp.send('reload'); resolve(); }); } function startWatcher() { return new Promise((resolve) => { const watcher = watch({ root: cwd, patterns }); globalWatcher = watcher; watcher.on('change', ({ path: fullpath }) => { const relpath = path.relative(cwd, fullpath); const filepath = path.resolve(cwd, relpath); const type = 'change'; if (overrideSignal === ERRORED) { log.info(`file ${type}: ${relpath}`); return restartApp(); } if (!globalApp) { log.info(`file ${type}: ${relpath}`); return startApp(); } if (appfiles[filepath]) { log.info(`main file ${type}: ${relpath}`); globalApp.send('reset'); } else { log.info(`renderer file ${type}: ${relpath}`); globalApp.send('reload'); } }); watcher.on('add', ({ path: fullpath }) => { const relpath = path.relative(cwd, fullpath); log.verbose('watching new file:', relpath); }); watcher.once('ready', () => { log.info('waiting for a change to restart it'); resolve(); }); }); } function destroyApp() { return Promise.all([ closeApp(), globalWatcher.close() ]).then(() => { globalApp = globalWatcher = null; }); } function init() { return Promise.all([ globalWatcher ? Promise.resolve() : startWatcher(), globalApp ? Promise.resolve() : startApp() ]).then(() => undefined); } return init().then(() => ({ close: closeApp, destroy: destroyApp, reload: reloadApp, restart: restartApp })); };