File system watch does not work with mounted volumes

I used this in gulp4 to work around the issue.

Usage:

import gulp from 'gulp'
import gls from 'gulp-live-server'
import { build } from './build'
import { scan } from './ghetto-watch'

let _server = gls(['-r', 'babel-register', 'index.js'])

function start (callback) {
  _server.start()
  callback()
}

function stop (callback) {
  _server.stop()
  if (callback) callback()
}

function notify (callback) {
  _server.notify()
  callback()
}

function watch () {
  let src = [
    '*.js',
    'api/**/*',
    'site/**/*'
  ]

  let scanner = scan(src)
  let full = gulp.series(
    scanner.stop.bind(scanner),
    build,
    notify,
    stop,
    start,
    scanner.start.bind(scanner)
  )

  scanner.on('changed', (changed) => {
    console.log('changed:', changed)
    full()
  })
  scanner.start()
}

const run = gulp.series(build, start, watch)
gulp.task('start', run)
gulp.task('default', run)
// ghetto-watch.js
import fs from 'fs'
import path from 'path'
import map from 'async/map'
import mm from 'micromatch'
import { EventEmitter } from 'events'

// This should not exist but docker volumes for windows do not trigger file system events
// So instead I scan all the files looking for modify time changes and manually trigger events

let cache = {}
let token = null

function flatten(arr) {
  return arr.reduce(function (flat, toFlatten) {
    return flat.concat(Array.isArray(toFlatten) ? flatten(toFlatten) : toFlatten);
  }, []);
}

function scanFiles (file, callback) {
  fs.lstat(file, (err, stat) => {
    if (err) return callback(err)
    if (stat.isDirectory()) {
      let dir = file
      fs.readdir(dir, (err, files) => {
        if (err) return callback(err)
        let resolved = files.map(f => path.join(dir, f))
        map(resolved, scanFiles, (err, results) => {
          if (err) return callback(err)
          let changed = flatten(results.filter(f => f))
          callback(null, changed)
        })
      })
    } else if (stat.isFile()) {
      let mtime = stat.mtime.getTime()
      let changed = cache[file] != null && cache[file] !== mtime
      cache[file] = mtime
      callback(null, changed ? file : null)
    } else {
      callback()
    }
  })
}

export class Scanner extends EventEmitter {
  constructor (src) {
    super()
    this.running = false
    this.src = src.map(p => path.resolve(p))
  }

  scan (file, callback) {
    if (!this.running) return callback()
    fs.lstat(file, (err, stat) => {
      if (err) return callback(err)
      if (stat.isDirectory()) {
        let dir = file
        fs.readdir(dir, (err, files) => {
          if (err) return callback(err)
          let resolved = files.map(f => path.join(dir, f))
          map(resolved, scanFiles, (err, results) => {
            if (err) return callback(err)
            let changed = flatten(results.filter(f => f))
            callback(null, changed)
          })
        })
      } else if (stat.isFile()) {
        let mtime = stat.mtime.getTime()
        let changed = cache[file] != null && cache[file] !== mtime
        cache[file] = mtime
        callback(null, changed ? file : null)
      } else {
        callback()
      }
    })
  }

  run () {
    if (this.running) {
      this.scan(process.cwd(), (err, changed) => {
        if (err) console.error(err)
        if (mm(changed, this.src).length) this.emit('changed') // a change matching src glob happened!
        setTimeout(() => this.run(), 0)
      })
    }
  }

  start (callback) {
    if (!this.running) {
      this.running = true
      this.run()
    }
    if (callback) callback()
  }

  stop (callback) {
    this.running = false
    if (callback) callback()
  }
}

export function scan (src) {
  return new Scanner(src)
}