Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Asynchronous template compilation.
logger = require('onelog').get('AsyncTemplates')
_ = require 'underscore'
sinon = require 'sinon'
async = require 'async'
hamlc = require 'haml-coffee'
# You MUST specify the ALL names of the methods which return asynchronously.
# This is required because sync helpers can be used in conditionals.
# This is useful for retrofitting existing code, or keeping code clean
# and portable.
asyncMethods = ['js', 'css', 'asset', 'assetPath']
hamlcAssetHelperRegex = /(@js|@css|@asset)(.?)'(.*)'/gi
# Extract required assets from template by regex.
exports.requiredAssets = (str) =>
matches = []
match = hamlcAssetHelperRegex.exec str
break unless match?
matches.push match[3] if match.length >= 4
return matches
# TODO: Provide option to manually specify which helpers are asynchronous.
exports.renderAsync = (opts, cb) =>
data =
realLocals = opts.locals
# Method that compiles a template to a function that compiles locals to html.
# `string` -> `(locals) -> html`
templater = opts.templater
# Method that compiles a template and locals to html.
# `(data, locals) -> html`
compiler = opts.compiler
start = new Date()
# Pre-render template.
locals = _.clone realLocals
# Only stub asynchronous calls. Synchronous helpers maybe used in conditional
# statements with nested async helpers.
# NOTE: Cannot use async helpers for conditionals for blocks containing more
# async helpers.
for name, fn of locals
if _.contains asyncMethods, name
sinon.stub locals, name
sinon.spy locals, name
# Compile template method.
tmpl = null
if templater?
tmpl = templater data
tmpl = (locals) -> compiler data, locals
# 1st run to spy on which methods from locals are required for rendering.
tmpl locals
evaluatedLocals = {} # [method name][ordering of call]
tasks = [] # Async tasks to be run.
for name, spy of locals
if spy.called
# Evaluate local for each time it was called.
for i in [0..spy.callCount - 1]
do (name, spy, i) ->
name: "#{name}##{i}"
args: spy.args[i]
run: (taskFinished) ->
done = (err, val) ->
return taskFinished err if err
evaluatedLocals[name] = {} unless evaluatedLocals[name]?
# Store the evaluated method from each call.
evaluatedLocals[name][i] = do (val) -> val
logger.trace "Evaluating #{name}() with args:", spy.args[i]
# We need a way to check if a method is sync or async.
# No callback will be called if its sync.
# We could require all helpers to be async, however the majority of
# helpers are synchronous, so it should be opt-in from the template.
val = undefined
if _.isFunction _.last(spy.args[i])
# Template helper is asynchronous.
val = realLocals[name] _.initial(spy.args[i])..., done
else if _.contains asyncMethods, name
# Template helper is also asynchronous, but last arg is not
# a function, the name of the method was preset as async.
val = realLocals[name] spy.args[i]..., done
val = realLocals[name] spy.args[i]...
done null, val
# Wait until all locals have been evaluated.
async.forEach tasks, (task, done) ->
logger.trace 'Running task', done
, (err) ->
if err
logger.error err
return cb err
# Create a new stub that responds to our calls with the evaluated local
# for the n-th call.
stubLocals = {}
for name in _.keys evaluatedLocals
# Create a closure to share `n` amongst all invocations of each
# method: `name`. This allows us to pass different args to the same
# method call.
do (name) ->
n = 0
stubLocals[name] = ->
#logger.trace "Rendered #{name}##{n}", evaluatedLocals[name][n]
str = evaluatedLocals[name][n]
return str
# 2nd run to get html.
html = tmpl stubLocals
duration = (new Date() - start) / 1000 "Rendered template in #{duration}s"
cb null, html
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.