UNPKG

asset-rack

Version:

Static Web Framework for Nodejs

220 lines (176 loc) 7.11 kB
# Asset.coffee - The Asset class is the core abstraction for the framework # Pull in our dependencies async = require 'async' crypto = require 'crypto' pathutil = require 'path' fs = require 'fs' zlib = require 'zlib' mime = require 'mime' {extend} = require './util' {EventEmitter} = require 'events' # IE8 Compatibility mime.types.js = 'text/javascript' mime.extensions['text/javascript'] = 'js' # Asset class handles compilation and a lot of other functionality class exports.Asset extends EventEmitter # Default max age is set to one year defaultMaxAge: 60*60*24*365 constructor: (options) -> super() options ?= {} # Set the url @url = options.url if options.url? # Set the cotents if given @contents = options.contents if options.contents? # Set headers if given @headers = if options.headers then options.headers else {} headers = {} for key, value of @headers headers[key.toLowerCase()] = value @headers = headers # Get the extension from the url @ext = pathutil.extname @url # Set whether to watch or not @watch = options.watch @watch ?= false # Set the correct mimetype @mimetype = options.mimetype if options.mimetype? @mimetype ?= mime.types[@ext.slice(1, @ext.length)] @mimetype ?= 'text/plain' # Whether to gzip the asset or not @gzip = options.gzip # Whether to hash the url or not or both @hash = options.hash if options.hash? # Max age for HTTP cache control @maxAge = options.maxAge if options.maxAge? # Whether to allow caching of non-hashed urls @allowNoHashCache = options.allowNoHashCache if options.allowNoHashCache? # Fire callback if someone listens for a "complete" event # and it has already been called @on 'newListener', (event, listener) => if event is 'complete' and @completed is true listener() # This event is triggered after the contents have been created @on 'created', (data) => # If content then it's a single asset if data?.contents? @contents = data.contents # If assets then it's a mutil asset if data?.assets? @assets = data.assets # If this is a single asset then do some post processing if @contents? @createSpecificUrl() @createHeaders() # If it's a muti asset then make sure they are all completed if @assets? async.forEach @assets, (asset, done) -> asset.on 'error', done asset.on 'complete', done , (error) => return @emit 'error', error if error? @completed = true @emit 'complete' else @completed = true # Handles gzipping if @gzip zlib.gzip @contents, (error, gzip) => @gzipContents = gzip @emit 'complete' else @emit 'complete' # Does the file watching if @watch @watcher = fs.watch @toWatch, (event, filename) => if event is 'change' @watcher.close() @completed = false @assets = false process.nextTick => @emit 'start' # Listen for errors and throw if no listeners @on 'error', (error) => throw error if @listeners('error') is 1 @on 'start', => @maxAge ?= @rack?.maxAge @maxAge ?= @defaultMaxAge unless @hash is false @allowNoHashCache ?= @rack?.allowNoHashCache @create options # Next tick because we need to wait on a possible rack process.nextTick => # Setting max age for HTTP cache control @maxAge ?= @defaultMaxAge # Create the asset unless it is part of a rack # then the rack will trigger the "start" event return @create options unless @rack? # Add an asset for multi asset support addAsset: (asset) -> @assets = [] unless @assets? @assets.push asset # Responds to an express route respond: (request, response) -> headers = {} if request.path is @url and @allowNoHashCache isnt true for key, value of @headers headers[key] = value delete headers['cache-control'] else headers = @headers for key, value of headers response.header key, value if @gzip response.send @gzipContents else response.send @contents # Check if a given url "matches" this asset checkUrl: (url) -> url is @specificUrl or (not @hash? and url is @url) # Used so that an asset can be express middleware handle: (request, response, next) -> handle = => if @assets? for asset in @assets if asset.checkUrl request.path return asset.respond request, response if @checkUrl(request.path) @respond request, response else next() if @completed is true handle() else @on 'complete', -> handle() # Default create method, usually overwritten create: (options) -> # At the end of a create method you always call # the created event @emit 'created' # Create the headers for an asset createHeaders: -> @headers['content-type'] ?= "#{@mimetype}" if @gzip @headers['content-encoding'] ?= 'gzip' if @maxAge? @headers['cache-control'] ?= "public, max-age=#{@maxAge}" # Gets the HTML tag for an asset tag: -> switch @mimetype when 'text/javascript' tag = "\n<script type=\"#{@mimetype}\" " return tag += "src=\"#{@specificUrl}\"></script>" when 'text/css' return "\n<link rel=\"stylesheet\" href=\"#{@specificUrl}\">" # Creates and md5 hash of the url for caching createSpecificUrl: -> @md5 = crypto.createHash('md5').update(@contents).digest 'hex' # This is the no hash option if @hash is false @useDefaultMaxAge = false return @specificUrl = @url # Construction of the hashed url @specificUrl = "#{@url.slice(0, @url.length - @ext.length)}-#{@md5}#{@ext}" # Might need a hostname if not on same server if @hostname? @specificUrl = "//#{@hostname}#{@specificUrl}" # For extending this class in javascript # for coffeescript you can use the builtin extends @extend: extend