forked from arne/asdf-games
255 lines
8.1 KiB
JavaScript
255 lines
8.1 KiB
JavaScript
|
// TODO: Expose this like webpack-dev-server middleware
|
||
|
var stacked = require('stacked')
|
||
|
var serveStatic = require('serve-static')
|
||
|
var defaultIndex = require('simple-html-index')
|
||
|
var logger = require('./simple-http-logger')
|
||
|
var urlLib = require('url')
|
||
|
var xtend = require('xtend')
|
||
|
var pushState = require('connect-pushstate')
|
||
|
var liveReload = require('inject-lr-script')
|
||
|
var urlTrim = require('url-trim')
|
||
|
var escapeHtml = require('escape-html')
|
||
|
|
||
|
var fs = require('fs')
|
||
|
var browserify = require('browserify')
|
||
|
var path = require('path')
|
||
|
var liveReloadClientFile = path.resolve(__dirname, 'reload', 'client.js')
|
||
|
var bundledReloadClientFile = path.resolve(__dirname, '..', 'build', 'bundled-livereload-client.js')
|
||
|
|
||
|
// Patch 'wasm' since send has not yet been updated to latest 'mime' module
|
||
|
serveStatic.mime.types['wasm'] = 'application/wasm'
|
||
|
|
||
|
module.exports = budoMiddleware
|
||
|
function budoMiddleware (entryMiddleware, opts) {
|
||
|
opts = opts || {}
|
||
|
var staticPaths = [].concat(opts.dir).filter(Boolean)
|
||
|
if (staticPaths.length === 0) {
|
||
|
staticPaths = [ process.cwd() ]
|
||
|
}
|
||
|
|
||
|
var entrySrc = opts.serve
|
||
|
var live = opts.live
|
||
|
var cors = opts.cors
|
||
|
var handler = stacked()
|
||
|
var middlewares = [].concat(opts.middleware).filter(Boolean)
|
||
|
|
||
|
// Everything is logged except favicon.ico
|
||
|
var ignoreLog = [].concat(opts.ignoreLog).filter(Boolean)
|
||
|
var logHandler = logger({
|
||
|
ignores: [ '/favicon.ico' ].concat(ignoreLog)
|
||
|
})
|
||
|
handler.use(function (req, res, next) {
|
||
|
if (cors) {
|
||
|
res.setHeader('Access-Control-Allow-Headers', 'Cache-Control, Pragma, Origin, Authorization, Content-Type, X-Requested-With')
|
||
|
res.setHeader('Access-Control-Allow-Methods', 'GET, PUT, POST')
|
||
|
res.setHeader('Access-Control-Allow-Origin', '*')
|
||
|
}
|
||
|
logHandler(req, res, next)
|
||
|
})
|
||
|
|
||
|
// User middleware(s) can override others
|
||
|
middlewares.forEach(function (middleware) {
|
||
|
if (typeof middleware !== 'function') {
|
||
|
throw new Error('middleware options must be functions')
|
||
|
}
|
||
|
handler.use(function (req, res, next) {
|
||
|
logHandler.type = 'middleware'
|
||
|
middleware(req, res, next)
|
||
|
})
|
||
|
})
|
||
|
|
||
|
// Entry (watchify) middleware
|
||
|
if (entryMiddleware) {
|
||
|
var entryRoute = urlLib.parse(entrySrc).pathname
|
||
|
if (!/^\//.test(entryRoute)) entryRoute = '/' + entryRoute
|
||
|
handler.use(function (req, res, next) {
|
||
|
if (urlTrim(req.url) === urlTrim(entryRoute)) {
|
||
|
entryMiddleware(req, res, next)
|
||
|
} else {
|
||
|
next()
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// Re-route for pushState support
|
||
|
if (opts.pushstate) {
|
||
|
if (typeof opts.pushstate === 'string') {
|
||
|
throw new Error('--pushstate is a string, you shouold use subarg options instead')
|
||
|
}
|
||
|
var pushStateOpts = xtend(typeof opts.pushstate === 'boolean' ? {} : opts.pushstate)
|
||
|
delete pushStateOpts._ // added by subarg
|
||
|
handler.use(pushState(pushStateOpts))
|
||
|
}
|
||
|
|
||
|
// Inject liveReload snippet on response
|
||
|
var liveInjector = liveReload({
|
||
|
local: true
|
||
|
})
|
||
|
// this is lazily set and cannot be changed dynamically
|
||
|
var liveScriptUrl
|
||
|
// By default, attempt to optimize the response
|
||
|
var shouldUseBundledLiveReload = true
|
||
|
// Cache the client by default to optimize the response
|
||
|
var liveReloadClient
|
||
|
handler.use(liveReloadHandler)
|
||
|
|
||
|
// Ignore favicon clutter
|
||
|
handler.mount('/favicon.ico', favicon)
|
||
|
|
||
|
// If the user wishes to *always* serve
|
||
|
// a generated index instead of a static one.
|
||
|
if (opts.forceDefaultIndex) {
|
||
|
handler.use(indexHandler)
|
||
|
}
|
||
|
|
||
|
// Static assets (html/images/etc)
|
||
|
staticPaths.forEach(function (rootFile) {
|
||
|
var staticOpts = xtend({
|
||
|
cacheControl: false
|
||
|
}, opts.staticOptions)
|
||
|
delete staticOpts._ // from subarg
|
||
|
|
||
|
var staticHandler = serveStatic(rootFile, staticOpts)
|
||
|
handler.use(function (req, res, next) {
|
||
|
logHandler.type = 'static'
|
||
|
staticHandler(req, res, next)
|
||
|
})
|
||
|
})
|
||
|
|
||
|
// Generates a default index.html
|
||
|
// when none is found locally.
|
||
|
handler.use(indexHandler)
|
||
|
|
||
|
// Handle errors
|
||
|
handler.use(function (req, res) {
|
||
|
res.statusCode = 404
|
||
|
res.end('404 not found: ' + escapeHtml(req.url))
|
||
|
})
|
||
|
|
||
|
// Allow live options to be changed at runtime
|
||
|
handler.setLiveOptions = setLiveOptions
|
||
|
return handler
|
||
|
|
||
|
function setLiveOptions (opts) {
|
||
|
live = opts
|
||
|
}
|
||
|
|
||
|
function favicon (req, res) {
|
||
|
var maxAge = 345600 // 4 days
|
||
|
res.setHeader('Cache-Control', 'public, max-age=' + Math.floor(maxAge / 1000))
|
||
|
res.setHeader('Content-Type', 'image/x-icon')
|
||
|
res.statusCode = 200
|
||
|
res.end()
|
||
|
}
|
||
|
|
||
|
function indexHandler (req, res, next) {
|
||
|
if (urlLib.parse(req.url).pathname === '/' || /\/index.html?/i.test(req.url)) {
|
||
|
// If we reach this, our response will be generated
|
||
|
// (not static from local file system)
|
||
|
logHandler.type = 'generated'
|
||
|
res.setHeader('content-type', 'text/html')
|
||
|
|
||
|
var stream = opts.defaultIndex || defaultIndex
|
||
|
stream({
|
||
|
entry: entrySrc,
|
||
|
title: opts.title,
|
||
|
css: opts.css,
|
||
|
base: opts.base === true ? '/' : (opts.base || null)
|
||
|
}, req).pipe(res)
|
||
|
} else {
|
||
|
next()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function serveBundledLiveReload (res, successCallback) {
|
||
|
if (liveReloadClient) {
|
||
|
res.end(liveReloadClient)
|
||
|
successCallback(true)
|
||
|
} else {
|
||
|
fs.readFile(bundledReloadClientFile, function (err, src) {
|
||
|
if (err) {
|
||
|
if (shouldUseBundledLiveReload) {
|
||
|
console.error([
|
||
|
'NOTE: The bundled LiveReload client could not be found, so budo will',
|
||
|
'generate this on the fly.',
|
||
|
'This is most likely because you are using a git cloned version of budo',
|
||
|
'instead of installing it from npm.',
|
||
|
'You can run this from your cloned budo folder to create a build:',
|
||
|
' npm run bundle-live-client'
|
||
|
].join('\n'))
|
||
|
}
|
||
|
shouldUseBundledLiveReload = false
|
||
|
successCallback(false)
|
||
|
} else {
|
||
|
liveReloadClient = src
|
||
|
res.end(src)
|
||
|
successCallback(true)
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function serveBrowserifyLiveReload (cache, debug, liveScripts, res) {
|
||
|
// Browserify the client file, e.g. if user has a script to include
|
||
|
if (cache && liveReloadClient) {
|
||
|
res.end(liveReloadClient)
|
||
|
} else {
|
||
|
var b = browserify({ debug: debug })
|
||
|
b.add(liveReloadClientFile)
|
||
|
if (live.expose) {
|
||
|
b.require(liveReloadClientFile, { expose: 'budo-livereload' })
|
||
|
}
|
||
|
|
||
|
liveScripts.forEach(function (file) {
|
||
|
b.add(path.resolve(file))
|
||
|
})
|
||
|
b.bundle(function (err, src) {
|
||
|
if (err) {
|
||
|
console.error('Error bundling LiveReload client:\n' + err.message)
|
||
|
res.statusCode = 500
|
||
|
res.end('Error bundling LiveReload client: ' + err)
|
||
|
} else {
|
||
|
liveReloadClient = src
|
||
|
res.end(src)
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function liveReloadHandler (req, res, next) {
|
||
|
if (!live || live.plugin) return next()
|
||
|
if (!liveScriptUrl) {
|
||
|
liveScriptUrl = live.path || '/budo/livereload.js'
|
||
|
logHandler.ignores.push(liveScriptUrl)
|
||
|
} else if (liveScriptUrl && live.path && liveScriptUrl !== live.path) {
|
||
|
var errMessage = 'Error: The LiveReload path field cannot be changed dynamically.\n' +
|
||
|
'Please open an issue in budo if you have a specific use case for this.'
|
||
|
console.error(errMessage)
|
||
|
res.statusCode = 500
|
||
|
res.end(errMessage)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if (req.url === liveScriptUrl) {
|
||
|
res.statusCode = 200
|
||
|
res.setHeader('Content-Type', 'application/javascript')
|
||
|
var liveScripts = (Array.isArray(live.include) ? live.include : [ live.include ]).filter(Boolean)
|
||
|
var cache = live.cache !== false
|
||
|
var debug = live.debug
|
||
|
|
||
|
// Default setup - use a bundled JS file for LiveReload client
|
||
|
if (shouldUseBundledLiveReload && cache && !debug && liveScripts.length === 0) {
|
||
|
serveBundledLiveReload(res, function (success) {
|
||
|
// fall back to browserify on the fly
|
||
|
if (!success) serveBrowserifyLiveReload(cache, debug, liveScripts, res)
|
||
|
})
|
||
|
} else {
|
||
|
serveBrowserifyLiveReload(cache, debug, liveScripts, res)
|
||
|
}
|
||
|
} else {
|
||
|
liveInjector.path = liveScriptUrl
|
||
|
liveInjector(req, res, next)
|
||
|
}
|
||
|
}
|
||
|
}
|