codesharing.coffee | |
---|---|
coffeescript = require "coffee-script"
fs = require "fs"
path = require "path"
{minify, beautify} = require "./minify" | |
Remove last comma from string | removeTrailingComma = (s) ->
s.trim().replace(/,$/, "") |
Return current unix time | getCurrentTimestamp = -> (new Date()).getTime() |
Timestamp of the time when this server was started. Used for killing browser caches. | startTime = getCurrentTimestamp() |
Map of functions that can convert various Javascript objects to strings. | types =
function: (fn) -> "#{ fn }"
string: (s) -> "\"#{ s }\""
number: (n) -> n.toString()
boolean: (n) -> n.toString()
object: (obj) -> |
typeof reports array as object | return this._array obj if Array.isArray obj
code = "{"
code += "\"#{ k }\": #{ codeFrom v }," for k, v of obj
removeTrailingComma(code) + "}"
_array: (array) ->
code = "["
code += " #{ codeFrom v }," for v in array
removeTrailingComma(code) + "]" |
Generates code string from given object. Works for numbers, strings, regexes and even functions. Does not handle circular references. | codeFrom = (obj) ->
types[typeof obj]?(obj) |
Converts map of variables and array of functions to executable Javascript code | isolatedCodeFrom = (vars, execs, context) ->
code = "(function(){\n" |
variables are not always set when using reponses Declare local variables | variableNames = (name for name, _ of vars).join(", ")
if variableNames.length > 0
code += "var #{ removeTrailingComma variableNames };\n" |
Set local and context variables | for name, variable of vars
code += "#{ name } = this.#{ name } = #{ codeFrom variable };\n" |
Create immediately functions from the function array | for fn in execs
code += executableFrom fn, context |
Set context of this closure. ie. "this" | code += "}).call(#{ context });\n"
return code |
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"
wrapInScriptTagInline = (code) ->
"<script type=\"text/javascript\" >\n#{ code }\n</script>\n" |
Wraps given URI to a script tag. Will kill browser cache using timestamp query string if killcache is true. | wrapInScriptTag = (uri, killcache) ->
timestamp = if killcache then getCurrentTimestamp() else startTime
"<script type=\"text/javascript\" src=\"#{ uri }?v=#{ timestamp }\"></script>"
exports.addCodeSharingTo = (app) -> |
Set default scripts-directory | if not app.set "clientscripts"
app.set "clientscripts", "#{ process.cwd() }/clientscripts" |
Path to a directory of client-side only scripts. | scriptDir = app.set "clientscripts" |
All code that is embedded in Node.js code that will be sent to browser. | compiledEmbeddedCode = null |
All client-side code in production (except externals). | productionClientCode = "" |
Cache for compiled script-tags | compiledTags = null |
Array of client-side script names | try
base = path.basename scriptDir
clientScriptsFs = ("#{ base }/#{ script }" for script in fs.readdirSync(scriptDir).sort())
catch err |
Directory is just missing | clientScriptsFs = []
nsClientScriptsFs = [] |
Array of external client-side script urls. | scriptURLs = [] |
Variables that are shared with Node.js and browser on every page | clientVars = {} |
Namespaced client variables | nsClientVars = [] |
Functions that will executed immediately in browser when loaded. | clientExecs = [] |
Namespaced client functions | nsClientExecs = [] |
Function for getting all script tags. Configure will create this. | getScriptTags = null |
List of functions that are ran when the app starts listening a port. | runOnListen = [] |
Run when app starts listening a port | app.on 'listening', ->
fn() for fn in runOnListen |
Collect shared variables and code and wrap them in a closure for browser execution. | runOnListen.push -> |
Create common namespace for shared code | compiledEmbeddedCode = "window._SC = {};\n"
compiledEmbeddedCode += isolatedCodeFrom clientVars, clientExecs, "_SC"
compiledEmbeddedCode = beautify compiledEmbeddedCode
app.configure "development", -> |
Development version getScriptTags | getScriptTags = (req) -> |
External client scripts. CDNs etc. | tags = (wrapInScriptTag url, true for url in scriptURLs) |
Client scripts on filesystem | for script in clientScriptsFs
script = script.trim().replace(/\.coffee$/, ".js")
tags.push wrapInScriptTag "/managedjs/dev/#{ script }", true |
Embedded scripts | tags.push wrapInScriptTag "/managedjs/embedded.js", true
return tags.join("\n") + "\n"
app.configure "production", -> |
Production version of getScriptTags | getScriptTags = (req) ->
return compiledTags if compiledTags |
External client scripts | tags = (wrapInScriptTag url for url in scriptURLs) |
Everything else is bundled in production.js | tags.push wrapInScriptTag "/managedjs/production.js"
return compiledTags = tags.join "\n"
app.configure "production", -> |
We will allow usage of production.js only in production mode | runOnListen.push ->
for script in clientScriptsFs
if script.match /\.js$/
productionClientCode += fs.readFileSync(script).toString()
else if script.match /\.coffee$/
productionClientCode += coffeescript.compile fs.readFileSync(script).toString()
productionClientCode += compiledEmbeddedCode
productionClientCode = minify productionClientCode |
All js code will be shared from here in production | app.get "/managedjs/production.js", (req, res) -> |
Cache for an year. Server restarts will reset the cache. | res.setHeader 'Cache-Control', 'public, max-age=31556926'
res.send productionClientCode, 'Content-Type': 'application/javascript' |
Exposed as share on response object. Works like app.share but for only this one response | responseShare = (obj) -> |
"this" is the response object | localVars = this.localVars
for k, v of obj
localVars[k] = v
return obj |
Same as responseShare but for executable code | responseExec = (fn) -> |
"this" is the response object | this.localExecs.push fn |
Middleware that adds share & exec methods to response objects. | app.use (req, res, next) ->
res.localVars ?= {}
res.localExecs ?= []
res.share = responseShare
res.exec = responseExec
next() |
Dynamic helper for templates. bundleJavascript will return all required script-tags. | app.dynamicHelpers renderScriptTags: (req, res) ->
return ->
bundle = getScriptTags(req)
for ns in nsClientScriptsFs
if ns.matcher.exec req.url
bundle += wrapInScriptTagInline ns.code
for ns in nsClientExecs
if ns.matcher.exec req.url |
ns execs will be executed before reponse execs | res.localExecs.unshift ns.fn
for ns in nsClientVars
if ns.matcher.exec req.url
for k, v of ns.obj |
ns vars cannot override locals | res.localVars[k] = v if not res.localVars[k]
localCode = isolatedCodeFrom res.localVars, res.localExecs, "_SC"
bundle += wrapInScriptTagInline localCode
return bundle |
Extends Express server object with function that will share given Javascript object with browser. Will work for functions too, but be sure that you will use only pure functions. Scope or context will not be same in the browser. Variables will added as local variables in browser and also in to _SC object. | app.share = (ns) ->
obj = arguments[arguments.length-1]
if ns is obj
for k, v of obj
clientVars[k] = v
else
nsClientVars.push matcher: ns, obj: obj
return obj
app.shareFs = (ns) ->
filePath = arguments[arguments.length-1]
if ns is filePath
clientScriptsFs.push filePath
else
obj = matcher: ns
nsClientScriptsFs.push obj
fs.readFile filePath, (err, data) ->
throw err if err
obj.code = minify data.toString() |
Extends Express server object with function that will executed given function in the browser as soon as it is loaded. | app.exec = (ns, fn) ->
fn = arguments[arguments.length-1]
if ns is fn
clientExecs.push(fn)
else
nsClientExecs.push matcher: ns, fn: fn
return fn |
Extends Express server object with function that will execute given Javascript URL in the browser as soon as it is loaded. | app.scriptURL = (obj) ->
if Array.isArray obj
scriptURLs.unshift url for url in obj.reverse()
else
scriptURLs.unshift obj
app.configure "development", -> |
All client-side code embedded in node.js code will shared from here. | app.get "/managedjs/embedded.js", (req, res) ->
res.send compiledEmbeddedCode, 'Content-Type': 'application/javascript'
responseWith404 = (res, scriptName) ->
res.send "Could not find script #{ scriptName }.js"
, ('Content-Type': 'text/plain'), 404 |
Client-side only script are shared from here. | app.get /^\/managedjs\/dev\/(.+)\.js/, (req, res) -> |
TODO: we should only compile when file really changed by checking modified timestamp. Does this really matter in development-mode? | scriptName = req.params[0]
if scriptName + ".js" in clientScriptsFs
fs.readFile "#{ scriptName }.js", (err, data) ->
if not err
res.send data, 'Content-Type': 'application/javascript'
else
responseWith404 res, scriptName
else if scriptName + ".coffee" in clientScriptsFs
fs.readFile "#{ scriptName }.coffee", (err, data) ->
if not err
res.send coffeescript.compile(data.toString())
, 'Content-Type': 'application/javascript'
else
responseWith404 res, scriptName
else
responseWith404 res, scriptName
return app
|