expresser
Version:
A ready to use Node.js web app wrapper, built on top of Express.
252 lines (250 loc) • 8.99 kB
JavaScript
var url = require("url")
var http = require("http")
var fs = require("fs")
var pathModule = require("path")
var mime = require("mime")
var Assets = (module.exports = function(Mincer, options) {
if (!options) {
options = Mincer
Mincer = require("./mincer.js")
}
this.options = options
if (this.options.compile) {
this.environment = new Mincer.Environment()
this.environment.ContextClass.defineAssetPath(
this.helper(function(url) {
return url
})
)
if (this.options.compress) {
this.environment.cssCompressor = "csswring"
this.environment.jsCompressor = "uglify"
}
if (this.options.sourceMaps) {
this.environment.enable("source_maps")
}
this.options.paths.forEach(this.environment.appendPath, this.environment)
if (this.options.build) {
this.manifest = new Mincer.Manifest(this.environment, this.options.buildDir)
}
} else {
this.manifest = new Mincer.Manifest(null, this.options.buildDir)
}
})
Assets.prototype.compile = function(callback) {
var instance = this
if (!this.options.compile) {
return callback()
}
if (this.manifest) {
try {
var manifestObj = this.manifest.compile(this.options.precompile, {
compress: this.options.gzip,
sourceMaps: this.options.sourceMaps,
embedMappingComments: this.options.embedMappingComments,
noSourceMapProtection: this.options.noSourceMapProtection
})
callback(null, manifestObj)
} catch (err) {
callback(err)
}
} else {
this.environment.eachLogicalPath(this.options.precompile, function(path) {
try {
instance.environment.findAsset(path)
} catch (err) {
// Swallow silently -- when the asset helper is used later the error
// will be raised again.
}
})
process.nextTick(callback)
}
}
Assets.prototype.serveAsset = function(req, res, next) {
var path = parseUrl(req.url).pathname.replace(/^\//, "")
path = path.substr(this.options.localServePath.length).replace(/^\//, "")
path = decodeURIComponent(path)
if (req.method !== "GET" && req.method !== "HEAD") {
res.writeHead(405)
return res.end(http.STATUS_CODES[405])
}
if (isInvalidPath(path)) {
res.writeHead(400)
return res.end(http.STATUS_CODES[400])
}
var parts = parse(path)
var asset, assetName
try {
asset = this.getAssetByPath(parts.path, {
bundle: this.options.bundle
})
if (isPossibleSourceMapPath(path) && !asset) {
var originalAssetPath = parts.path.substring(0, parts.path.length - 4)
asset = this.getAssetByPath(originalAssetPath, {
bundle: this.options.bundle
})
if (!asset || !asset.sourceMap) {
return next()
}
var sourceMap = asset.sourceMap.toString()
res.setHeader("Content-Length", sourceMap.length)
res.setHeader("Content-Type", "application/json")
return res.end(sourceMap)
}
if (!asset || (this.options.fingerprinting && asset.digest !== parts.fingerprint)) {
return next()
}
if (this.manifest && this.manifest.assets[parts.path]) {
var assetName = this.manifest.assets[parts.path]
asset.mtime = new Date(asset.mtime)
asset.length = asset.size
asset.contentType = mime.getType(pathModule.join(this.options.buildDir, assetName))
}
} catch (err) {
return next(err)
}
res.setHeader("Cache-Control", "public, max-age=31536000")
res.setHeader("Date", new Date().toUTCString())
res.setHeader("Last-Modified", asset.mtime.toUTCString())
res.setHeader("ETag", '"' + asset.digest + '"')
if (req.headers["if-none-match"] === '"' + asset.digest + '"') {
res.writeHead(304)
return res.end()
}
if (this.options.sourceMaps && asset.sourceMap) {
var sourceMapPath = pathModule.join(this.options.localServePath, path + ".map")
if (sourceMapPath.indexOf("http") !== 0) {
sourceMapPath = "/" + sourceMapPath
}
res.setHeader("X-SourceMap", sourceMapPath)
}
var contentType = asset.contentType
if (contentType) {
if (contentType.match(/^text\/|\/json$|\/javascript$/)) {
contentType += "; charset=UTF-8"
}
res.setHeader("Content-Type", contentType)
}
res.setHeader("Content-Length", asset.length)
if (req.method === "HEAD") {
return res.end()
}
if (!asset.buffer) {
var localFilePath = pathModule.join(this.options.buildDir, assetName)
return fs.readFile(localFilePath, function(err, data) {
if (err) return next(err)
res.end(data)
})
} else {
res.end(asset.buffer)
}
}
Assets.prototype.helper = function(tagWriter, ext) {
var instance = this
return function(path, options) {
var asset
var regExp = new RegExp("\\." + ext + "$")
var pathHasNoExt = !regExp.test(path)
if (ext && pathHasNoExt) {
path = path + "." + ext
}
asset = instance.getAssetByPath(path, {
bundle: true
})
if (!asset) {
var searchPath = instance.options.compile ?
"search path:\n " + instance.environment.__trail__.paths.join("\n ") :
"manifest:\n " + instance.options.buildDir + "/manifest.json"
throw new Error("Asset '" + path + "' not found in " + searchPath)
}
var getTag = function(path, asset) {
var servePath = instance.options.servePath
var path = servePath + "/" + (asset.logicalPath || asset.logical_path)
var attributes = parseAttributes(options)
if (!isAbsolutePath(servePath)) {
path = "/" + path
}
if (instance.options.fingerprinting) {
path = path.replace(/(\.[^.]+)$/, "-" + asset.digest + "$1")
}
var contentProvider = instance.getAssetContentSync.bind(instance, asset)
return tagWriter(path, contentProvider, attributes)
}
if (!instance.options.bundle && asset.type === "bundled") {
var assets = asset.toArray()
var tags = []
for (var i = 0; i < assets.length; i++) {
tags.push(getTag(path, assets[i]))
}
return tags.join("\n")
} else {
return getTag(path, asset)
}
}
}
Assets.prototype.getAssetByPath = function(pathname, options) {
var asset
if (this.manifest && this.manifest.assets[pathname]) {
var assetTitle = this.manifest.assets[pathname]
asset = this.manifest.files[assetTitle]
}
if (!asset && this.options.compile) {
return this.environment.findAsset(pathname, options)
}
return asset
}
Assets.prototype.getAssetContentSync = function(asset) {
if (asset.buffer) {
return asset.buffer
}
if (this.manifest) {
var path = asset.logicalPath || asset.logical_path
if (path && this.manifest.assets[path]) {
return fs.readFileSync(pathModule.join(this.options.buildDir, this.manifest.assets[path]))
}
}
return ""
}
var isInvalidPath = function(pathname) {
return pathname.indexOf("..") !== -1 || pathname.indexOf("\u0000") !== -1
}
var isAbsolutePath = function(pathname) {
return pathname.indexOf("http") === 0 || pathname.indexOf("//") === 0
}
var isPossibleSourceMapPath = function(pathname) {
var ext = ".map"
return pathname.indexOf(ext) === pathname.length - ext.length
}
var parse = function(path) {
var fingerprint = /-([0-9a-f]{32,40})(\.[A-Za-z0-9.]+)$/
var parts = path.match(fingerprint)
return {
fingerprint: parts ? parts[1] : null,
path: path.replace(fingerprint, "") + (parts ? parts[2] : "")
}
}
var parseAttributes = function(attributes) {
attributes = attributes || {}
var attrs = []
for (var name in attributes) {
var value = attributes[name]
switch (typeof value) {
case "boolean":
if (value) {
attrs.push(name)
}
break
case "string":
attrs.push(name + '="' + value + '"')
break
default:
continue
}
}
return attrs.join(" ")
}
var parseUrl = function(string) {
var parseQueryString = false
var allowUrlWithoutProtocol = true
return url.parse(string, parseQueryString, allowUrlWithoutProtocol)
}