simplywatch
Version:
Watches files and upon change executes a command for each file INDIVIDUALLY with file-related params
189 lines (137 loc) • 5.14 kB
text/coffeescript
Promise = require 'bluebird'
fs = require 'fs-jetpack'
Path = require 'path'
execa = require 'execa'
chalk = require 'chalk'
md5 = require 'md5'
promiseBreak = require 'promise-break'
isBinary = require './helpers/isBinary'
isModule = require './helpers/isModule'
SimplyImport = require 'simplyimport'
debug =
file: require('debug')('simplywatch:file')
imports: require('debug')('simplywatch:imports')
class File extends require('events')
@badFilesDeps = Object.create(null) # For tracking deps of files that were provided without an extension and didn't exist in the referenced directory
@get = ({filePath, watchContext, canSkipRescan}, settings, task)->
settings.fileCache[filePath]?.process(canSkipRescan) or
settings.fileCache[filePath] = new File(filePath, watchContext, settings, task)
constructor: (@filePath, @watchContext, @settings, @task)->
super()
@path = Path.relative process.cwd(), @filePath
@pathDebug = chalk.dim @path
@dir = Path.dirname(@path)
@fileDir = Path.dirname(@filePath)
@fileExt = @resolveExtension()
@filePathNoExt = @fileDir+'/'+Path.basename(@filePath, @fileExt)
@isCoffee = @fileExt is '.coffee'
@relDir = Path.dirname(@watchContext)
@relDir = if @relDir[0] is '.' then @relDir.slice(1) else @relDir
@relDir = @dir.replace @relDir, ''
@pathParams = Path.parse @filePath
@pathParams.path = @filePath
@pathParams.reldir = @relDir.slice(1)
@isBinary = isBinary(@filePath)
@deps = File.badFilesDeps[@filePathNoExt] or []
@imports = []
@lastExecuted = null
@lastScanned = null
debug.file "new file #{@pathDebug}"
return @process()
process: (canSkipRescan)->
return @ if @isBinary or canSkipRescan and @content
@scanProcedure =
Promise.bind(@)
.then(@getContents)
.then(@scanImports)
return @
resolveExtension: ()->
extension = Path.extname(@filePath)
if extension
return extension
else
try
thisFileName = Path.basename(@filePath)
files = fs.list(@fileDir)
for filePath in files
fileExt = Path.extname(filePath)
fileName = Path.basename(filePath, fileExt)
if fileName is thisFileName
extension = fileExt
break
if extension
@filePath += extension
@path += extension
@settings.fileCache[@filePath] = @
return extension or ''
getContents: ()->
if not @fileExt
Promise.resolve()
else
Promise.bind(@)
.then ()-> fs.readAsync(@filePath)
.then (content)-> @hash = md5(@content=content)
.catch ()->
checkIfImportsFile: (targetFile, deepScan=true)->
iteratedArrays = [@imports]
checkArray = (importsArray)=>
importsArray.includes(targetFile) or
importsArray.find (currentFile)->
if iteratedArrays.includes(currentFile.imports)
return false
else
iteratedArrays.push(currentFile.imports)
return if deepScan then checkArray(currentFile.imports) else false
checkArray(@imports)
scanImports: ()->
Promise.bind(@)
.then ()->
if @settings.scanCache[@hash]
debug.imports "using cached scan #{@pathDebug}"
@imports = @settings.scanCache[@hash].map (filePath)=>
childFile = File.get({filePath, @watchContext, canSkipRescan:true}, @settings, @task)
childFile.deps.push(@) unless childFile.deps.includes(@) or childFile.checkIfImportsFile(@)
@task.emit('childFile', childFile)
return childFile
promiseBreak()
else if @canScanImports
debug.file "scanning #{@pathDebug}"
@lastScanned = Date.now()
@imports.length = 0
else
promiseBreak()
.then ()->
SimplyImport.scan file:@filePath, src:@content, flat:false, depth:0
.then (imports)->
imports.forEach (child)=>
debug.imports "found #{chalk.dim Path.relative @dir,child.file} in #{@pathDebug}"
if isModule(child.file, @settings)
return debug.imports "skipping #{chalk.dim Path.relative @dir,child.file} because it's an external module"
childPath = child.file
childFile = File.get({filePath:childPath, @watchContext}, @settings, @task)
if not childFile.fileExt # Indicates provided childPath didn't have a file extension and has yet to be discovered. Delete from cache so that next time a discovery will be re-attempted
File.badFilesDeps[childPath] ?= []
File.badFilesDeps[childPath].push @
delete @settings.fileCache[childPath]
@task.emit('childFile', childFile)
@imports.push(childFile)
childFile.deps.push(@) unless childFile.deps.includes(@) or childFile.checkIfImportsFile(@)
.then ()->
@settings.scanCache[@hash] = @imports.map (childFile)-> childFile.filePath
if @imports.length
Promise.map @imports, (childFile)-> childFile.scanProcedure
else
debug.imports "0 imports found in #{@pathDebug}"
.catch promiseBreak.end
canExecuteCommand: (invokeTime)->
if @lastExecuted
return invokeTime - @lastExecuted > @settings.execDelay
else
return true
Object.defineProperties @::,
canScanImports: get: ()->
if @lastScanned
return Date.now() - @lastScanned > 500
else
return true
module.exports = File