332 lines
8.7 KiB
JavaScript
332 lines
8.7 KiB
JavaScript
|
var bole = require('bole')
|
||
|
var xtend = require('xtend')
|
||
|
var once = require('once')
|
||
|
var path = require('path')
|
||
|
var EventEmitter = require('events').EventEmitter
|
||
|
var isMatch = require('micromatch')
|
||
|
var openUrl = require('opn')
|
||
|
var internalIp = require('internal-ip')
|
||
|
var garnish = require('garnish')
|
||
|
|
||
|
var defaults = require('./parse-args').defaults
|
||
|
var getPorts = require('./get-ports')
|
||
|
var createServer = require('./server')
|
||
|
var createBundler = require('./bundler')
|
||
|
var createFileWatch = require('./file-watch')
|
||
|
var createReloadServer = require('./reload/server')
|
||
|
var mapEntry = require('./map-entry')
|
||
|
|
||
|
var noop = function () {}
|
||
|
|
||
|
module.exports = createBudo
|
||
|
function createBudo (entries, opts) {
|
||
|
var log = bole('budo')
|
||
|
|
||
|
// if no entries are specified, just options
|
||
|
if (entries && !Array.isArray(entries) && typeof entries === 'object') {
|
||
|
opts = entries
|
||
|
entries = []
|
||
|
}
|
||
|
|
||
|
// do not mutate user options
|
||
|
opts = xtend({}, defaults, { stream: false }, opts)
|
||
|
entries = entries || []
|
||
|
|
||
|
// perhaps later this will be configurable
|
||
|
opts.cwd = process.cwd()
|
||
|
|
||
|
// log to output stream
|
||
|
if (opts.stream) {
|
||
|
// by default, pretty-print to the stream with info logging
|
||
|
if (!opts.ndjson) {
|
||
|
var pretty = garnish({
|
||
|
level: opts.verbose ? 'debug' : 'info',
|
||
|
name: 'budo'
|
||
|
})
|
||
|
pretty.pipe(opts.stream)
|
||
|
opts.stream = pretty
|
||
|
}
|
||
|
|
||
|
bole.output({
|
||
|
stream: opts.stream,
|
||
|
level: 'debug'
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// optionally allow as arrays
|
||
|
entries = [].concat(entries).filter(Boolean)
|
||
|
|
||
|
var entryObjects = entries.map(mapEntry)
|
||
|
var entryFiles = entryObjects.map(function (entry) {
|
||
|
return entry.from
|
||
|
})
|
||
|
|
||
|
if (opts.serve && typeof opts.serve !== 'string') {
|
||
|
throw new TypeError('opts.serve must be a string or undefined')
|
||
|
} else if (!opts.serve && entries.length > 0) {
|
||
|
opts.serve = entryObjects[0].url
|
||
|
}
|
||
|
|
||
|
// default to cwd
|
||
|
if (!opts.dir || opts.dir.length === 0) {
|
||
|
opts.dir = opts.cwd
|
||
|
}
|
||
|
|
||
|
var emitter = new EventEmitter()
|
||
|
var bundler, middleware
|
||
|
|
||
|
if (entries.length > 0 || (opts.browserify && opts.browserify.entries)) {
|
||
|
bundler = createBundler(entryFiles, opts)
|
||
|
middleware = bundler.middleware
|
||
|
|
||
|
bundler.on('log', function (ev) {
|
||
|
if (ev.type === 'bundle') {
|
||
|
var time = ev.elapsed
|
||
|
ev.elapsed = time
|
||
|
ev.name = 'browserify'
|
||
|
ev.type = undefined
|
||
|
ev.colors = {
|
||
|
elapsed: time > 1000 ? 'yellow' : 'dim',
|
||
|
message: 'dim '
|
||
|
}
|
||
|
log.info(ev)
|
||
|
}
|
||
|
})
|
||
|
|
||
|
// uncaught syntax errors should not stop the server
|
||
|
// this only happens when errorHandler: false
|
||
|
bundler.on('error', function (err) {
|
||
|
console.error('Error:', err.message ? err.message : err)
|
||
|
})
|
||
|
bundler.on('bundle-error', emitter.emit.bind(emitter, 'bundle-error'))
|
||
|
bundler.on('update', emitter.emit.bind(emitter, 'update'))
|
||
|
bundler.on('pending', emitter.emit.bind(emitter, 'pending'))
|
||
|
|
||
|
emitter.on('update', function (contents, deps) {
|
||
|
if (deps.length > 1) {
|
||
|
log.debug({
|
||
|
name: 'browserify',
|
||
|
message: deps.length + ' files changed'
|
||
|
})
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
var defaultInternalIp = internalIp.v4.sync()
|
||
|
var defaultWatchGlob = opts.watchGlob || '**/*.{html,css}'
|
||
|
var server = null
|
||
|
var closed = false
|
||
|
var started = false
|
||
|
var fileWatcher = null
|
||
|
var reloader = null
|
||
|
var deferredWatch = noop
|
||
|
var deferredLive = noop
|
||
|
|
||
|
// public API
|
||
|
emitter.close = once(close)
|
||
|
emitter.reload = reload
|
||
|
emitter.error = errorPopup
|
||
|
emitter.live = live
|
||
|
emitter.watch = watch
|
||
|
|
||
|
// setup defaults for live reload / watchify
|
||
|
if (opts.live) {
|
||
|
var initialLiveOpts = typeof opts.live === 'object' ? opts.live : undefined
|
||
|
var initialLiveMatch = typeof opts.live === 'string' ? opts.live : undefined
|
||
|
if (initialLiveMatch) {
|
||
|
emitter.once('connect', function () {
|
||
|
log.info({ message: 'LiveReload filtering filenames with glob:', url: initialLiveMatch })
|
||
|
if (entryObjects.length === 0) {
|
||
|
log.info({ message: '\nNOTE: It looks like you are using budo without a JavaScript entry.\n' +
|
||
|
' This is fine, but if you were trying to bundle the "' + initialLiveMatch + '" file,\n you should re-arrange' +
|
||
|
' your arguments like so:\n\n' +
|
||
|
' budo ' + initialLiveMatch + ' --live' })
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
emitter
|
||
|
.watch()
|
||
|
.live(initialLiveOpts)
|
||
|
.on('watch', function (ev, file) {
|
||
|
if (ev !== 'change' && ev !== 'add') {
|
||
|
return
|
||
|
}
|
||
|
defaultFileEvent(file)
|
||
|
})
|
||
|
.on('pending', function () {
|
||
|
defaultFileEvent(opts.serve)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// First, setup a server
|
||
|
createServer(middleware, xtend(opts, { ip: defaultInternalIp }), function (err, serverInstance) {
|
||
|
if (err) {
|
||
|
emitter.emit('error', err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
server = serverInstance
|
||
|
|
||
|
// start portfinding + connect
|
||
|
getPorts(opts, handlePorts)
|
||
|
})
|
||
|
|
||
|
return emitter
|
||
|
|
||
|
function defaultFileEvent (file) {
|
||
|
var filename = path.basename(file)
|
||
|
if ((Array.isArray(opts.live) || typeof opts.live === 'string') &&
|
||
|
isMatch(filename, opts.live).length === 0) {
|
||
|
return
|
||
|
}
|
||
|
emitter.reload(file)
|
||
|
}
|
||
|
|
||
|
function reload (file) {
|
||
|
process.nextTick(emitter.emit.bind(emitter, 'reload', file))
|
||
|
if (reloader) {
|
||
|
reloader.reload(file)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function errorPopup (message) {
|
||
|
if (reloader) {
|
||
|
reloader.errorPopup(message)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// enable file watch capabilities
|
||
|
function watch (glob, watchOpt) {
|
||
|
if (!started) {
|
||
|
deferredWatch = emitter.watch.bind(null, glob, watchOpt)
|
||
|
} else {
|
||
|
// destroy previous
|
||
|
if (fileWatcher) fileWatcher.close()
|
||
|
glob = glob && glob.length > 0 ? glob : defaultWatchGlob
|
||
|
glob = Array.isArray(glob) ? glob : [ glob ]
|
||
|
watchOpt = xtend({ poll: opts.poll }, watchOpt)
|
||
|
|
||
|
fileWatcher = createFileWatch(glob, watchOpt)
|
||
|
fileWatcher.on('watch', emitter.emit.bind(emitter, 'watch'))
|
||
|
}
|
||
|
return emitter
|
||
|
}
|
||
|
|
||
|
// enables LiveReload capabilities
|
||
|
function live (liveOpts) {
|
||
|
if (!started) {
|
||
|
deferredLive = emitter.live.bind(null, liveOpts)
|
||
|
} else {
|
||
|
// destroy previous
|
||
|
if (reloader) reloader.close()
|
||
|
|
||
|
// pass some options for the server middleware
|
||
|
server.setLiveOptions(xtend(liveOpts))
|
||
|
|
||
|
// create a web socket server for live reload
|
||
|
reloader = createReloadServer(server, opts)
|
||
|
}
|
||
|
return emitter
|
||
|
}
|
||
|
|
||
|
function getHostAddress (host) {
|
||
|
// user can specify "::" or "0.0.0.0" as host exactly
|
||
|
// or if undefined, default to internal-ip
|
||
|
if (!host) {
|
||
|
host = server.address().address
|
||
|
if (host === '0.0.0.0') {
|
||
|
// node 0.10 returns this when no host is specified
|
||
|
// node 0.12 returns internal-ip
|
||
|
host = '::'
|
||
|
}
|
||
|
}
|
||
|
if (host === '::') {
|
||
|
host = defaultInternalIp
|
||
|
}
|
||
|
if (!host) {
|
||
|
host = '127.0.0.1'
|
||
|
}
|
||
|
return host
|
||
|
}
|
||
|
|
||
|
function handlePorts (err, result) {
|
||
|
if (closed) return
|
||
|
if (err) {
|
||
|
emitter.emit('error', err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
opts.port = result.port
|
||
|
|
||
|
// improve error messaging
|
||
|
server.on('error', function (err) {
|
||
|
if (err.code === 'EADDRINUSE') {
|
||
|
err.message = 'port ' + opts.port + ' is in use'
|
||
|
emitter.emit('error', err)
|
||
|
} else {
|
||
|
emitter.emit('error', err)
|
||
|
}
|
||
|
})
|
||
|
|
||
|
// start server
|
||
|
// no host -> use localhost + internal-ip
|
||
|
server.listen(opts.port, opts.host || undefined, connect)
|
||
|
}
|
||
|
|
||
|
function connect () {
|
||
|
if (closed) return
|
||
|
started = true
|
||
|
|
||
|
// default host is internal IP
|
||
|
opts.host = getHostAddress(opts.host)
|
||
|
|
||
|
var port = opts.port
|
||
|
var protocol = opts.ssl ? 'https' : 'http'
|
||
|
var uri = protocol + '://' + opts.host + ':' + port + '/'
|
||
|
|
||
|
log.info({ message: 'Server running at', url: uri, type: 'connect' })
|
||
|
|
||
|
// if live() or watch() was called before connection
|
||
|
deferredWatch()
|
||
|
deferredLive()
|
||
|
|
||
|
// provide info on server connection
|
||
|
emitter.emit('connect', {
|
||
|
uri: uri,
|
||
|
port: port,
|
||
|
host: opts.host,
|
||
|
serve: opts.serve,
|
||
|
entries: entryFiles,
|
||
|
server: server,
|
||
|
webSocketServer: reloader ? reloader.webSocketServer : undefined,
|
||
|
dir: opts.dir
|
||
|
})
|
||
|
|
||
|
// initial bundle should come after
|
||
|
// connect event!
|
||
|
if (bundler) bundler.bundle()
|
||
|
|
||
|
// launch browser
|
||
|
if (opts.open) {
|
||
|
openUrl(uri)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function close () {
|
||
|
var next = emitter.emit.bind(emitter, 'exit')
|
||
|
if (started) {
|
||
|
server.once('close', next)
|
||
|
} else {
|
||
|
process.nextTick(next)
|
||
|
}
|
||
|
|
||
|
if (started) bole.reset()
|
||
|
if (started) server.close()
|
||
|
if (reloader) reloader.close()
|
||
|
if (bundler) bundler.close()
|
||
|
if (fileWatcher) fileWatcher.close()
|
||
|
closed = true
|
||
|
started = false
|
||
|
}
|
||
|
}
|