Skip to content

Instantly share code, notes, and snippets.

@emirotin
Last active December 13, 2015 17:28
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save emirotin/4948454 to your computer and use it in GitHub Desktop.
Save emirotin/4948454 to your computer and use it in GitHub Desktop.
connect-assets helpers for CDN support
express = require 'express'
asset_compilers = require './asset-compilers'
config = require './config'
app = express()
app_root = __dirname
static_path = asset_compilers.static_path
app.set 'view engine', 'jade'
app.locals.pretty = true
app.set 'views', path.join app_root, 'views'
app
.use(express.bodyParser())
.use(express.methodOverride())
.use(asset_compilers.connect_assets_middleware) # won't be triggered in production as all requests will go to CDN
.use(express.static static_path)
.use(app.router)
# start server
app.listen(process.env.PORT or 3000)
shell = require 'shelljs'
path = require 'path'
jade = require 'jade'
connect_assets = require 'connect-assets'
S = require 'string'
app_root = __dirname
exports.static_path = static_path = path.join app_root, 'client'
js_dir = path.join static_path, 'javascripts'
sassCompiler =
optionsMap: { }
compileSync: (sourcePath, source) ->
if sourcePath.indexOf static_path == 0
sourcePath = sourcePath[static_path.length + 1..]
cmd = "cd \"#{static_path}\" && sass --compass -r susy \"#{sourcePath}\""
shell.exec(cmd, silent: true).output
cjadeCompiler =
optionsMap: { }
compileSync: (sourcePath, source) ->
filepath = path.normalize(sourcePath).replace js_dir, ''
if filepath[0] == path.sep
filepath = filepath[1..]
filepath = filepath.replace /\\/g, '\/'
filepath = filepath.substring 0, filepath.lastIndexOf '.'
preamble = """
window.JST = window.JST || {};
JST['#{filepath}'] = function
"""
compiled = '' + jade.compile(source, client: true, filename: sourcePath, pretty: true, debug: false)
compiled.replace('function anonymous', preamble) + ";"
assets_context = {}
exports.connect_assets_middleware = connect_assets helperContext: assets_context, src: 'client', jsCompilers: cjade: cjadeCompiler
assets_context.css.root = exports.stylesheetsDir = 'stylesheets'
assets_context.js.root = exports.javascriptsDir = 'javascripts'
connect_assets.cssCompilers.sass = sassCompiler
IS_PROD = process.env.NODE_ENV == 'production'
if IS_PROD
compiled_assets_mapping = JSON.parse process.env.COMPILED_ASSETS
STATIC_HOST = require('./config').STATIC_HOST
normalize_route = (route, type) ->
Sroute = S(route)
if route.match '^((https?:)?//)|(www\.)'
return [route]
if type == 'js'
prefix = exports.javascriptsDir
ext = '.js'
else
prefix = exports.stylesheetsDir
ext = '.css'
route = prefix + '/' + route
if not Sroute.endsWith(ext)
route += ext
if compiled_assets_mapping[route]
routes = compiled_assets_mapping[route]
else
routes = ['/' + route]
("#{STATIC_HOST}#{r}" for r in routes)
global.js = (route, routeOptions) ->
if IS_PROD
routes = normalize_route route, 'js'
loadingKeyword = ''
if routeOptions?
loadingKeyword = 'async ' if routeOptions.async?
loadingKeyword = 'defer ' if routeOptions.defer?
("<script #{loadingKeyword}src='#{r}'></script>" for r in routes).join '\n'
else
assets_context.js.apply null, arguments
global.css = (route) ->
if IS_PROD
routes = normalize_route route, 'css'
("<link rel='stylesheet' href='#{r}'>" for r in routes).join '\n'
else
assets_context.css.apply null, arguments
exports.assets_instance = connect_assets.instance
path = require 'path'
fs = require 'fs'
fse = require 'fs.extra'
S = require 'string'
asset_compilers = require('./asset-compilers')
asset_compilers_instance = asset_compilers.assets_instance
static_path = asset_compilers.static_path
buildDir = 'builtAssets'
buildDirFull = path.join __dirname, buildDir
asset_compilers_instance.options.build = true
asset_compilers_instance.options.minifyBuilds = false
asset_compilers_instance.options.buildDir = buildDir
compiled = {}
mode_build = true
process_file = (fname) ->
fullFname = fname
if fname.indexOf static_path == 0
fname = fname[static_path.length + 1..]
if fname[0] == '.'
return
mode = null
ext = path.extname fname
Sfname = S fname
if Sfname.startsWith(asset_compilers.stylesheetsDir) and ext == '.sass'
mode = 'css'
if Sfname.startsWith(asset_compilers.javascriptsDir) and ext in ['.coffee', '.jade', '.cjade']
mode = 'js'
if not mode
# processing plain files - skip if on the build step now
if mode_build
return
destFname = path.join buildDirFull, fname
fse.mkdirp path.dirname(destFname), (err) ->
if err then throw err
fse.copy fullFname, destFname, (err) ->
if err then throw err
console.log "Copied #{fname} -> #{destFname}"
else
# building assets - skip if in copy mode
if not mode_build
return
if path.basename(fname)[0] == '_'
return
re = RegExp (ext.replace '.', '\\.') + "$"
if mode == 'js'
fname = fname.replace re, '.js'
res = asset_compilers_instance.compileJS fname
else if mode == 'css'
fname = fname.replace re, '.css'
res = asset_compilers_instance.compileCSS fname
if not Array.isArray res
res = [res]
console.log "Compiled #{fname} -> #{res}"
compiled[fname] = res
compile_done = ->
compiled = JSON.stringify compiled
console.log 'Compiled mapping: ' + compiled
fs.open '.compiled-assets.json', 'w', (err, fd) ->
if err then throw err
fs.write fd, compiled
walk = (dir, onFile, onEnd) ->
walker = fse.walk(dir)
walker.on 'file', (root, fileStats, next) ->
onFile path.join root, fileStats.name
next()
walker.on 'end', onEnd
fse.remove buildDirFull, ->
console.log '* Compiling assets'
walk static_path, process_file, ->
console.log '* Copying assets'
mode_build = false
walk static_path, process_file, compile_done

The idea:

  • allow precompilation which outputs a set of compiled cache-busted files and produces a mapping object { source_path => compiled name }
  • make this mapping available to the production code through the environment variable (good for Heroku)
  • still support on-the-fly compilation for the development machine

asset-compilers.coffee is a adapter module around connect-assets and the "mapping" JSON. Also adds 2 custom compilers - a thin Sass wrapper around the official gem and a Jade compiler for client-side templates

build_assets.coffee is a CLI script using the previous module to perform the compilation and outputs the mapping JSON to a file. It also directly copies all the assets that do not need compilation

config.coffee is an example how STATIC_HOST is made available for the application

app.coffee shows how the assets middleware is plugged in

deploy.sh shows how assets are built for the production

view.jade shows how assets are linked from the view

config =
development:
STATIC_HOST: ''
production:
STATIC_HOST: '//static.mysite.com'
module.exports = config[process.env.NODE_ENV or 'development']
#!/usr/bin/env bash
# build assets
coffee build_assets.coffee
# put asssets to amazon
s3cmd -c .s3cfg sync --acl-public --guess-mime-type builtAssets/ s3://static.mysite.com
# push to heroku
git push heroku master
# pass env vars to heroku
# * NODE_ENV
# * COMPILED_ASSETS mappings between cache-busted urls and full
heroku config:add NODE_ENV=production COMPILED_ASSETS=`cat .compiled-assets.json`
block styles
!= css("content")
block scripts
!= js("content")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment