UNPKG

asset-rack

Version:

Static Web Framework for Nodejs

230 lines (194 loc) 8.29 kB
# Rack.coffee - A Rack is an asset manager # Pull in our dependencies async = require 'async' pkgcloud = require 'pkgcloud' fs = require 'fs' jade = require 'jade' pathutil = require 'path' {BufferStream, extend} = require('./util') {EventEmitter} = require 'events' # Rack - Manages multiple assets class exports.Rack extends EventEmitter constructor: (assets, options) -> super() # Set a default options object options ?= {} # Max age for HTTP Cache-Control @maxAge = options.maxAge # Allow non-hahshed urls to be cached @allowNoHashCache = options.allowNoHashCache # Once complete always set the completed flag @on 'complete', => @completed = true # If someone listens for the "complete" event # check if it's already been called @on 'newListener', (event, listener) => if event is 'complete' and @completed is true listener() # Listen for the error event, throw if no listeners @on 'error', (error) => console.log error @hasError = true @currentError = error # Give assets in the rack a reference to the rack for asset in assets asset.rack = this # Create a flattened array of assets @assets = [] # Do this in series for dependency conflicts async.forEachSeries assets, (asset, next) => # Listen for any asset error events asset.on 'error', (error) => next error # Wait for assets to finish completing asset.on 'complete', => # This is necessary because of asset recompilation return if @completed # If the asset has contents, it's a single asset if asset.contents? @assets.push asset # If it has assets, then it's multi-asset if asset.assets? @assets = @assets.concat asset.assets next() # This tells our asset to start asset.emit 'start' # Handle any errors for the assets , (error) => return @emit 'error', error if error? @emit 'complete' # Makes the rack function as express middleware handle: (request, response, next) -> response.locals assets: this if request.url.slice(0,11) is '/asset-rack' return @handleAdmin request, response, next if @hasError for asset in @assets check = asset.checkUrl request.path return asset.respond request, response if check return response.redirect '/asset-rack/error' handle = => for asset in @assets check = asset.checkUrl request.path return asset.respond request, response if check next() if @completed handle() else @on 'complete', handle handleError: (request, response, next) -> # No admin in production for now return next() if process.env.NODE_ENV is 'production' errorPath = pathutil.join __dirname, 'admin/templates/error.jade' fs.readFile errorPath, 'utf8', (error, contents) => return next error if error? compiled = jade.compile contents, filename: errorPath response.send compiled stack: @currentError.stack.split '\n' handleAdmin: (request, response, next) -> # No admin in production for now return next() if process.env.NODE_ENV is 'production' split = request.url.split('/') if split.length > 2 path = request.url.replace '/asset-rack/', '' if path is 'error' return @handleError request, response, next response.sendfile pathutil.join __dirname, 'admin', path else adminPath = pathutil.join __dirname, 'admin/templates/admin.jade' fs.readFile adminPath, 'utf8', (error, contents) => return next error if error? compiled = jade.compile contents, filename: adminPath response.send compiled assets: @assets # Writes a config file of urls to hashed urls for CDN use writeConfigFile: (filename) -> config = {} for asset in @assets config[asset.url] = asset.specificUrl fs.writeFileSync filename, JSON.stringify(config) # Deploy assets to a CDN deploy: (options, next) -> options.keyId = options.accessKey options.key = options.secretKey deploy = => client = pkgcloud.storage.createClient options assets = @assets # Big time hack for rackspace, first asset doesn't upload, very strange. # Might be bug with pkgcloud. This hack just uploads the first file again # at the end. assets = @assets.concat @assets[0] if options.provider is 'rackspace' async.forEachSeries assets, (asset, next) => stream = null headers = {} if asset.gzip stream = new BufferStream asset.gzipContents headers['content-encoding'] = 'gzip' else stream = new BufferStream asset.contents url = asset.specificUrl.slice 1, asset.specificUrl.length for key, value of asset.headers headers[key] = value headers['x-amz-acl'] = 'public-read' if options.provider is 'amazon' clientOptions = container: options.container remote: url headers: headers stream: stream client.upload clientOptions, (error) -> return next error if error? next() , (error) => if error? return next error if next? throw error if options.configFile? @writeConfigFile options.configFile next() if next? if @completed deploy() else @on 'complete', deploy # Creates an HTML tag for a given asset tag: (url) -> for asset in @assets return asset.tag() if asset.url is url throw new Error "No asset found for url: #{url}" # Gets the hashed url for a given url url: (url) -> for asset in @assets return asset.specificUrl if url is asset.url # Extend the class for javascript @extend: extend # The ConfigRack uses a json file and a hostname to map assets to a url # without actually compiling them class ConfigRack constructor: (options) -> # Check for required options throw new Error('options.configFile is required') unless options.configFile? throw new Error('options.hostname is required') unless options.hostname? # Setup our options @assetMap = require options.configFile @hostname = options.hostname # For hooking up as express middleware handle: (request, response, next) -> response.locals assets: this for url, specificUrl of @assetMap if request.path is url or request.path is specificUrl # Redirect to the CDN, the config does not have the files return response.redirect "//#{@hostname}#{specificUrl}" next() # Simple function to get the tag for a url tag: (url) -> switch pathutil.extname(url) when '.js' tag = "\n<script type=\"text/javascript\" " return tag += "src=\"//#{@hostname}#{@assetMap[url]}\"></script>" when '.css' return "\n<link rel=\"stylesheet\" href=\"//#{@hostname}#{@assetMap[url]}\">" # Get the hashed url for a given url url: (url) -> return "//#{@hostname}#{@assetMap[url]}" # Shortcut function exports.fromConfigFile = (options) -> return new ConfigRack(options)