Files
biomedjs/node_modules/piler/lib/piler.coffee
2014-09-14 07:04:16 -04:00

451 lines
9.8 KiB
CoffeeScript

fs = require "fs"
path = require "path"
crypto = require 'crypto'
path = require "path"
_ = require "underscore"
async = require "async"
{jsMinify, cssMinify} = require "./minify"
OB = require "./serialize"
compilers = require "./compilers"
assetUrlParse = require "./asseturlparse"
logger = require "./logger"
toGlobals = (globals) ->
code = ""
for nsString, v of globals
code += "__SET(#{ JSON.stringify nsString }, #{ OB.stringify v });\n"
code
extension = (filename) ->
parts = filename.split "."
parts[parts.length-1]
wrapInScriptTagInline = (code) ->
"<script type=\"text/javascript\" >\n#{ code }\n</script>\n"
getCompiler = (filePath) ->
compiler = compilers[extension filePath]
if not compiler
throw new Error "Could not find compiler for #{ filePath }"
compiler.render
#http://javascriptweblog.wordpress.com/2011/05/31/a-fresh-look-at-javascript-mixins/
asCodeOb = do ->
getId = ->
sum = crypto.createHash('sha1')
if @type is "file"
# If code is on filesystem the url to the file should only change when
# the path to it changes.
sum.update @filePath
else
# If there is no file for code code. We need to generate id from the code
# itself.
sum.update OB.stringify @
hash = sum.digest('hex').substring 10, 0
if @type in ["file", "module"]
filename = path.basename @filePath
filename = filename.replace /\./g, "_"
filename = filename.replace /\-/g, "_"
hash = filename + "_" + hash
return hash
pilers =
raw: (ob, cb) -> cb null, ob.raw
object: (ob, cb) ->
cb null, toGlobals ob.object
exec: (ob, cb) ->
cb null, executableFrom ob.object
file: (ob, cb) ->
fs.readFile ob.filePath, (err, data) =>
return cb? err if err
getCompiler(ob.filePath) ob.filePath, data.toString(), (err, code) ->
cb err, code
module: (ob, cb) ->
this.file ob, (err, code) ->
return cb? err if err
cb null, """require.register("#{ path.basename(ob.filePath).split(".")[0] }", function(module, exports, require) {
#{ code }
});"""
return ->
@getId = getId
@getCode = (cb) ->
pilers[@type] @, cb
return @
class BasePile
urlRoot: "/piler/"
production: false
constructor: (@name, @production, urlRoot) ->
if urlRoot?
@urlRoot = urlRoot
@code = []
@rawPile = null
@urls = []
@devMapping = {}
@piledUp = false
addFile: (filePath) ->
filePath = path.normalize filePath
@warnPiledUp "addFile"
if filePath not in @getFilePaths()
@code.push asCodeOb.call
type: "file"
filePath: filePath
addRaw: (raw) ->
@warnPiledUp "addRaw"
@code.push asCodeOb.call
type: "raw"
raw: raw
getFilePaths: ->
(ob.filePath for ob in @code when ob.type is "file")
addUrl: (url) ->
if url not in @urls
@urls.push url
getSources: ->
devCacheKey = Date.now()
# Start with plain urls
sources = ([u] for u in @urls)
if @production
sources.push ["#{ @urlRoot }min/#{ @pileHash }/#{ @name }.#{ @ext }"]
else
for ob in @code
sources.push ["#{ @urlRoot }dev/#{ devCacheKey }/#{ @name }.#{ ob.type }-#{ ob.getId() }.#{ @ext }", "id=\"pile-#{ ob.getId() }\""]
return sources
findCodeObById: (id) ->
(codeOb for codeOb in @code when codeOb.getId() is id)[0]
findCodeObByFilePath: (path) ->
(codeOb for codeOb in @code when codeOb.filePath is id)[0]
_computeHash: ->
sum = crypto.createHash('sha1')
sum.update @rawPile
@pileHash = sum.digest('hex')
warnPiledUp: (fnname) ->
if @piledUp
@logger.warn "Warning pile #{ @name } has been already piled up. Calling #{ fnname } does not do anything."
pileUp: (cb) ->
@piledUp = true
async.map @code, (codeOb, cb) =>
codeOb.getCode (err, code) =>
return cb? err if err
cb null, @commentLine("#{ codeOb.type }: #{ codeOb.getId() }") + "\n#{ code }"
, (err, result) =>
return cb? err if err
@rawPile = @minify result.join("\n\n").trim()
@_computeHash()
cb? null, @rawPile
class JSPile extends BasePile
ext: "js"
commentLine: (line) ->
return "// #{ line.trim() }"
minify: (code) ->
if @production
jsMinify code
else
code
constructor: ->
super
@objects = []
# CommonJS module
addModule: (filePath) ->
filePath = path.normalize filePath
@warnPiledUp "addFile"
if filePath not in @getFilePaths()
@code.push asCodeOb.call
type: "module"
filePath: filePath
addOb: (ob) ->
@warnPiledUp "addOb"
@code.push asCodeOb.call
type: "object"
object: ob
addExec: (fn) ->
@warnPiledUp "addExec"
@code.push asCodeOb.call
type: "exec"
object: fn
class CSSPile extends BasePile
ext: "css"
commentLine: (line) ->
return "/* #{ line.trim() } */"
minify: (code) ->
if @production
cssMinify code
else
code
defNs = (fn) ->
(ns, path) ->
if arguments.length is 1
path = ns
ns = "global"
fn.call this, ns, path
class PileManager
Type: null
constructor: (@settings) ->
@production = @settings.production
@settings.urlRoot ?= "/pile/"
@logger = @settings.logger || logger
@piles =
global: new @Type "global", @production, @settings.urlRoot
getPile: (ns) ->
pile = @piles[ns]
if not pile
pile = @piles[ns] = new @Type ns, @production, @settings.urlRoot
pile
addFile: defNs (ns, path) ->
pile = @getPile ns
pile.addFile path
addRaw: defNs (ns, raw) ->
pile = @getPile ns
pile.addRaw raw
addUrl: defNs (ns, url) ->
pile = @getPile ns
pile.addUrl url
pileUp: ->
logger = @logger
logger.notice "Start assets generation for '#{ @Type::ext }'"
for name, pile of @piles then do (pile) =>
pile.pileUp (err, code) =>
throw err if err
if @settings.outputDirectory?
outputPath = path.join @settings.outputDirectory, "#{ pile.name }.#{ pile.ext }"
fs.writeFile outputPath, code, (err) ->
throw err if err
logger.info "Wrote #{ pile.ext } pile #{ pile.name } to #{ outputPath }"
getSources: (namespaces...) ->
if typeof _.last(namespaces) is "object"
opts = namespaces.pop()
else
opts = {}
if not opts.disableGlobal
namespaces.unshift "global"
sources = []
for ns in namespaces
if pile = @piles[ns]
sources.push pile.getSources()...
return sources
renderTags: (namespaces...) ->
tags = ""
for src in @getSources namespaces...
tags += @wrapInTag src[0], src[1]
tags += "\n"
return tags
bind: (app, server=null) ->
@app = app
@server = server
listener = if server then server else app
listener.on "listening", =>
@pileUp()
@setMiddleware app
app.use (req, res, next) =>
if not _.startsWith req.url, @settings.urlRoot
return next()
res.setHeader "Content-type", @contentType
asset = assetUrlParse req.url
# Wrong asset type. Lets skip to next middleware.
if asset.ext isnt @Type::ext
return next()
pile = @piles[asset.name]
if not pile
res.send "Cannot find pile #{ asset.name }", 404
return
# TODO: set cache headers to forever
if asset.min
res.end pile.rawPile
return
codeOb = pile.findCodeObById asset.dev.uid
codeOb.getCode (err, code) ->
throw err if err
res.end code
return
class JSManager extends PileManager
Type: JSPile
contentType: "application/javascript"
constructor: ->
super
@piles.global.addExec ->
window._NS = (nsString) ->
parent = window
for ns in nsString.split "."
# Create new namespace if it is missing
parent = parent[ns] ?= {}
parent # return the asked namespace
window.__SET = (ns, ob) ->
parts = ns.split "."
if parts.length is 1
window[parts[0]] = ob
else
nsOb = _NS(parts.slice(0, -1).join("."))
target = parts.slice(-1)[0]
nsOb[target] = ob
wrapInTag: (uri, extra="") ->
"<script type=\"text/javascript\" src=\"#{ uri }\" #{ extra } ></script>"
addModule: defNs (ns, path) ->
pile = @getPile ns
pile.addModule path
addOb: defNs (ns, ob) ->
pile = @getPile ns
pile.addOb ob
addExec: defNs (ns, fn) ->
pile = @getPile ns
pile.addExec fn
setMiddleware: (app) ->
responseExec = (fn) ->
# "this" is the response object
this._responseFns.push fn
responseOb = (ob) ->
this._responseObs.push ob
# Middleware that adds add & exec methods to response objects.
app.use (req, res, next) ->
res._responseFns ?= []
res._responseObs ?= []
# TODO: deprecate res.exec
res.exec = res.addExec = responseExec
res.addOb = responseOb
next()
class CSSManager extends PileManager
Type: CSSPile
contentType: "text/css"
wrapInTag: (uri, extra="") ->
"<link rel=\"stylesheet\" href=\"#{ uri }\" #{ extra } />"
setMiddleware: (app) ->
# Creates immediately executable string presentation of given function.
# context will be function's "this" if given.
executableFrom = (fn, context) ->
return "(#{ fn })();\n" unless context
return "(#{ fn }).call(#{ context });\n"
LiveUpdateMixin = require "./livecss"
_.extend JSManager::, LiveUpdateMixin::
exports.production = production = process.env.NODE_ENV is "production"
exports.CSSPile = CSSPile
exports.JSPile = JSPile
exports.JSManager = JSManager
exports.CSSManager = CSSManager
exports.createJSManager = (settings={}) ->
settings.production = production
new JSManager settings
exports.createCSSManager = (settings={}) ->
settings.production = production
new CSSManager settings