import treeKill from "tree-kill" ; import { resolve , dirname , extname , basename } from "path" ; import { ChildProcess } from "child_process" ; import { spawn } from "cross-spawn" ; import chokidar from "chokidar" ; import { Writable , Readable } from "stream" ; import { Deque } from "@blakeembrey/deque" ; import { template , Template } from "@blakeembrey/template" ; const ECHO_JS_PATH = resolve ( __dirname , "echo.js" ) ; const ECHO_CMD = `${ quote ( process . execPath ) } ${ quote ( ECHO_JS_PATH ) }` ; /**
 * State used for `onchange` commands.
 */ interface State { event : string ; deprecated changed : string ; file : string ; fileExt : string ; fileBase : string ; fileBaseNoExt : string ; fileDir : string ; } /**
 * Execute a "job" on each change event.
 */ class Job { childCommand : ChildProcess | undefined ; childOutpipe : ChildProcess | undefined ; constructor ( public log , public command , public outpipe ? ) { } start ( cwd , stdin , stdout , stderr , env , onexit ) { if ( this . outpipe ) { const stdio = [ null , stdout , stderr ] ; this . log ( `executing outpipe "${ this . outpipe }"` ) ; this . childOutpipe = spawn ( this . outpipe , { cwd , env , stdio , shell : true } ) ; this . childOutpipe . on ( "exit" , ( code , signal ) => { this . log ( `outpipe ${ exitMessage ( code , signal ) }` ) ; this . childOutpipe = undefined ; if ( ! this . childCommand ) return onexit ( ) ; } ) ; } if ( this . command . length ) { const stdio = [ stdin , this . childOutpipe ? this . childOutpipe . stdin : stdout , stderr , ] ; this . log ( `executing command "${ this . command . join ( " " ) }"` ) ; this . childCommand = spawn ( this . command [ 0 ] , this . command . slice ( 1 ) , { cwd , env , stdio , } ) ; this . childCommand . on ( "exit" , ( code , signal ) => { this . log ( `command ${ exitMessage ( code , signal ) }` ) ; this . childCommand = undefined ; if ( ! this . childOutpipe ) return onexit ( ) ; return this . childOutpipe ?. stdin ?. end ( ) ; } ) ; } else { this . childOutpipe ?. stdin ?. end ( ) ; } } kill ( killSignal ? ) { if ( this . childOutpipe ) { this . log ( `killing outpipe ${ this . childOutpipe . pid }` ) ; treeKill ( this . childOutpipe . pid , killSignal ) ; } if ( this . childCommand ) { this . log ( `killing command ${ this . childCommand . pid }` ) ; treeKill ( this . childCommand . pid , killSignal ) ; } } } /**
 * Onchange configuration options.
 */ export interface Options { add ? : boolean ; awaitWriteFinish ? : number ; command ? : string [ ] ; cwd ? : string ; defaultExclude ? : boolean ; delay ? : number ; env ? : NodeJS . ProcessEnv ; exclude ? : ( string | ( ( path : string ) => boolean ) ) [ ] ; filter ? : string [ ] ; initial ? : boolean ; jobs ? : number ; kill ? : boolean ; killSignal ? : NodeJS . Signals ; matches : string [ ] ; onReady ? : ( ) => void ; outpipe ? : string ; poll ? : number ; stderr ? : Writable ; stdin ? : Readable ; stdout ? : Writable ; verbose ? : boolean ; } /**
 * Onchange manages
 */ export function onchange ( options ) { const { matches } = options ; const onReady = options . onReady || ( ( ) => undefined ) ; const initial = ! ! options . initial ; const kill = ! ! options . kill ; const cwd = options . cwd ? resolve ( options . cwd ) : process . cwd ( ) ; const stdin = options . stdin || process . stdin ; const stdout = options . stdout || process . stdout ; const stderr = options . stderr || process . stderr ; const env = options . env || process . env ; const delay = Math . max ( options . delay || 0 , 0 ) ; const jobs = Math . max ( options . jobs || 0 , 1 ) ; const killSignal = options . killSignal || "SIGTERM" ; const command = options . command ? options . command . map ( ( arg ) => template < State > ( arg ) ) : [ ] ; const outpipe = options . outpipe ? outpipeTemplate ( options . outpipe ) : undefined ; const filter = options . filter || [ ] ; const running = new Set < Job > ( ) ; const queue = new Deque < Job > ( ) ; const log = options . verbose ? function log ( message ) { stdout . write ( `onchange: ${ message }\n` ) ; } : function ( ) { } ; if ( command . length === 0 && ! outpipe ) { throw new TypeError ( 'Expected "command" and/or "outpipe" to be specified' ) ; } const ignored = options . exclude || [ ] ; const ignoreInitial = options . add !== true ; const usePolling = options . poll !== undefined ; const interval = options . poll !== undefined ? options . poll : undefined ; const awaitWriteFinish = options . awaitWriteFinish ? { stabilityThreshold : options . awaitWriteFinish } : undefined ; if ( options . defaultExclude !== false ) { ignored . push ( "**/node_modules/**" , "**/.git/**" ) ; } const watcher = chokidar . watch ( matches , { cwd , ignored , ignoreInitial , usePolling , interval , awaitWriteFinish , } ) ; /**
     * Try and dequeue the next job to run.
     */ function dequeue ( ) { if ( queue . size === 0 ) return ; if ( running . size >= jobs ) return ; const job = queue . popLeft ( ) ; running . add ( job ) ; job . start ( cwd , stdin , stdout , stderr , env , ( ) => { running . delete ( job ) ; if ( delay > 0 ) return setTimeout ( dequeue , delay ) ; return dequeue ( ) ; } ) ; } /**
     * Enqueue the next change event to run.
     */ function enqueue ( event , file ) { const fileExt = extname ( file ) ; const state = { event , changed : file , file , fileExt , fileBase : basename ( file ) , fileBaseNoExt : basename ( file , fileExt ) , fileDir : dirname ( file ) , } ; if ( kill ) { queue . clear ( ) ; running . forEach ( ( child ) => child . kill ( killSignal ) ) ; } log ( `${ file }: ${ event }` ) ; queue . push ( new Job ( log , command . map ( ( arg ) => arg ( state ) ) , outpipe ?. ( state ) ) ) ; return dequeue ( ) ; } if ( initial ) enqueue ( "" , "" ) ; watcher . on ( "all" , ( event , file ) => { if ( filter . length && filter . indexOf ( event ) === - 1 ) return ; return enqueue ( event , file ) ; } ) ; watcher . on ( "ready" , ( ) => { log ( `watcher ready` ) ; return onReady ( ) ; } ) ; watcher . on ( "error" , ( error ) => log ( `watcher error: ${ error }` ) ) ; return ( ) => watcher . close ( ) ; } function outpipeTemplate ( str ) { var value = str . trim ( ) ; if ( value . charAt ( 0 ) === "|" || value . charAt ( 0 ) === ">" ) { return template < State > ( `${ ECHO_CMD } ${ value }` ) ; } return template < State > ( value ) ; } function exitMessage ( code , signal ) { return code === null ? `exited with ${ signal }` : `completed with ${ code }` ; } /**
 * Quote value for `exec`.
 */ function quote ( str ) { return `"${ str . replace ( /["\\$`!]/g , "\\$&" ) }"` ; }