muffin.io
Version:
A full stack development tool for creating modern webapps
135 lines (112 loc) • 4.34 kB
text/coffeescript
# Watch the client folder for file changes and recompile as needed.
fs = require 'fs-extra'
sysPath = require 'path'
_ = require 'underscore'
async = require 'async'
chokidar = require 'chokidar'
logging = require './utils/logging'
project = require './project'
server = require './server'
# Files to ignore
ignored = (file) ->
filename = sysPath.basename(file)
/^\.|~$/.test(filename) or /\.swp/.test(filename)
# The `Watcher` class can watch for file changes, compile files, and remove files.
class Watcher
# Watch a directory
watchDir: (dir) ->
# Use `chokidar` for more reliable cross-platform file watching.
# Have to turn on 'polling' because FSWatcher has a lot of issues.
# Set polling interval to 200ms to reduce CPU usage.
watcher = chokidar.watch dir,
{ignored: ignored, persistent: true, ignoreInitial: true, usePolling: true, interval: 200}
watcher.on 'add', @compileFile
watcher.on 'change', @compileFile
watcher.on 'unlink', @removeFile
watcher.on 'error', (error) ->
logging.error "Error occurred while watching files: #{error}"
# Compile all the files in a directory and its subdirectories
compileDir: (dir, callback) ->
# Use a queue-based implementation to iterate over all files
queue = []
queue.push dir
test = -> queue.length > 0
fn = (done) =>
from = queue.pop()
stats = fs.statSync(from)
if stats.isDirectory()
files = fs.readdirSync(from)
files = files.filter (file) -> not ignored(file)
for file in files
queue.push sysPath.join(from, file)
done(null)
else if stats.isFile()
@compileFile from, yes, done
async.whilst test, fn, callback
# Compile a single file
compileFile: (path, abortOnError=no, callback) =>
destDir = @destDirForFile(path)
dest = @destForFile(path)
# Create destDir if it doesn't exist
fs.mkdirSync(destDir) unless fs.existsSync(destDir)
try
# Find a compiler plugin that can handle this file extension
ext = sysPath.extname(path)
compiler = @compilerForExt(ext)
if compiler
# Let the compiler plugin handle it.
compiler.compile path, destDir, ->
logging.info "compiled #{path}"
server.reloadBrowser(dest)
callback?(null)
else
# No plugins can handle this file. Simply copy it over.
fs.copy path, dest, ->
logging.info "copied #{path}"
server.reloadBrowser(dest)
callback?(null)
catch err
logging.error "#{err.message} (#{path})"
process.exit(1) if abortOnError
# Remove a file
removeFile: (path) =>
dest = @destForFile(path)
stats = fs.statSync(dest)
if stats.isFile()
fs.unlinkSync(dest)
logging.info "removed #{path}"
# Find a compiler plugin that can handle this file extension
compilerForExt: (ext) ->
for compiler in project.plugins.compilers
if ext in compiler.extensions
return compiler
return null
# The destDir for a file depends on where the file is located.
destDirForFile: (path) ->
inAssetsDir = (path.indexOf(project.clientAssetsDir) > -1)
inComponentsDir = (path.indexOf(project.clientComponentsDir) > -1)
if inAssetsDir
# If the file is in the `assets` folder, copy it to the buildDir.
relativePath = sysPath.relative(project.clientAssetsDir, path)
dest = sysPath.join(project.buildDir, relativePath)
else if inComponentsDir
# If the file is in the `components` folder, copy it to the buildDir.
relativePath = sysPath.relative(project.clientDir, path)
dest = sysPath.join(project.buildDir, relativePath)
else
# Otherwise, copy it to the jsDir.
relativePath = sysPath.relative(project.clientDir, path)
dest = sysPath.join(project.jsDir, relativePath)
return sysPath.dirname(dest)
# The dest for a file depends on the compiler that handles it.
destForFile: (path) ->
destDir = @destDirForFile(path)
# Find a compiler that can handle this file extension
ext = sysPath.extname(path)
compiler = @compilerForExt(ext)
if compiler
compiler.destForFile(path, destDir)
else
filename = sysPath.basename(path)
sysPath.join(destDir, filename)
module.exports = new Watcher()