UNPKG

6.64 kBtext/coffeescriptView Raw
1
2# Rack.coffee - A Rack is an asset manager
3
4# Pull in our dependencies
5async = require 'async'
6pkgcloud = require 'pkgcloud'
7fs = require 'fs'
8pathutil = require 'path'
9{BufferStream, extend} = require('./util')
10{EventEmitter} = require 'events'
11
12# Rack - Manages multiple assets
13class exports.Rack extends EventEmitter
14 constructor: (assets, options) ->
15 super()
16
17 # Set a default options object
18 options ?= {}
19
20 # Max age for HTTP Cache-Control
21 @maxAge = options.maxAge
22
23 # Allow non-hahshed urls to be cached
24 @allowNoHashCache = options.allowNoHashCache
25
26 # Once complete always set the completed flag
27 @on 'complete', =>
28 @completed = true
29
30 # If someone listens for the "complete" event
31 # check if it's already been called
32 @on 'newListener', (event, listener) =>
33 if event is 'complete' and @completed is true
34 listener()
35
36 # Listen for the error event, throw if no listeners
37 @on 'error', (error) =>
38 throw error if @listeners('error').length is 1
39
40 # Give assets in the rack a reference to the rack
41 for asset in assets
42 asset.rack = this
43
44 # Create a flattened array of assets
45 @assets = []
46
47 # Do this in series for dependency conflicts
48 async.forEachSeries assets, (asset, next) =>
49
50 # Listen for any asset error events
51 asset.on 'error', (error) =>
52 next error
53
54 # Wait for assets to finish completing
55 asset.on 'complete', =>
56
57 # This is necessary because of asset recompilation
58 return if @completed
59
60 # If the asset has contents, it's a single asset
61 if asset.contents?
62 @assets.push asset
63
64 # If it has assets, then it's multi-asset
65 if asset.assets?
66 @assets = @assets.concat asset.assets
67 next()
68
69 # This tells our asset to start
70 asset.emit 'start'
71
72 # Handle any errors for the assets
73 , (error) =>
74 return @emit 'error', error if error?
75 @emit 'complete'
76
77 # Makes the rack function as express middleware
78 handle: (request, response, next) ->
79 response.locals assets: this
80 handle = =>
81 for asset in @assets
82 check = asset.checkUrl request.path
83 return asset.respond request, response if check
84 next()
85 if @completed
86 handle()
87 else @on 'complete', handle
88
89 # Writes a config file of urls to hashed urls for CDN use
90 writeConfigFile: (filename) ->
91 config = {}
92 for asset in @assets
93 config[asset.url] = asset.specificUrl
94 fs.writeFileSync filename, JSON.stringify(config)
95
96 # Deploy assets to a CDN
97 deploy: (options, next) ->
98 options.keyId = options.accessKey
99 options.key = options.secretKey
100 deploy = =>
101 client = pkgcloud.storage.createClient options
102 assets = @assets
103 # Big time hack for rackspace, first asset doesn't upload, very strange.
104 # Might be bug with pkgcloud. This hack just uploads the first file again
105 # at the end.
106 assets = @assets.concat @assets[0] if options.provider is 'rackspace'
107 async.forEachSeries assets, (asset, next) =>
108 stream = null
109 headers = {}
110 console.log asset.url
111 if asset.gzip
112 stream = new BufferStream asset.gzipContents
113 headers['content-encoding'] = 'gzip'
114 else
115 stream = new BufferStream asset.contents
116 url = asset.specificUrl.slice 1, asset.specificUrl.length
117 for key, value of asset.headers
118 headers[key] = value
119 headers['x-amz-acl'] = 'public-read' if options.provider is 'amazon'
120 clientOptions =
121 container: options.container
122 remote: url
123 headers: headers
124 stream: stream
125 client.upload clientOptions, (error) ->
126 return next error if error?
127 next()
128 , (error) =>
129 if error?
130 return next error if next?
131 throw error
132 if options.configFile?
133 @writeConfigFile options.configFile
134 next() if next?
135 if @completed
136 deploy()
137 else @on 'complete', deploy
138
139 # Creates an HTML tag for a given asset
140 tag: (url) ->
141 for asset in @assets
142 return asset.tag() if asset.url is url
143 throw new Error "No asset found for url: #{url}"
144
145 # Gets the hashed url for a given url
146 url: (url) ->
147 for asset in @assets
148 return asset.specificUrl if url is asset.url
149
150 # Extend the class for javascript
151 @extend: extend
152
153# The ConfigRack uses a json file and a hostname to map assets to a url
154# without actually compiling them
155class ConfigRack
156 constructor: (options) ->
157 # Check for required options
158 throw new Error('options.configFile is required') unless options.configFile?
159 throw new Error('options.hostname is required') unless options.hostname?
160
161 # Setup our options
162 @assetMap = require options.configFile
163 @hostname = options.hostname
164
165 # For hooking up as express middleware
166 handle: (request, response, next) ->
167 response.locals assets: this
168 for url, specificUrl of @assetMap
169 if request.path is url or request.path is specificUrl
170
171 # Redirect to the CDN, the config does not have the files
172 return response.redirect "//#{@hostname}#{specificUrl}"
173 next()
174
175 # Simple function to get the tag for a url
176 tag: (url) ->
177 switch pathutil.extname(url)
178 when '.js'
179 tag = "\n<script type=\"text/javascript\" "
180 return tag += "src=\"//#{@hostname}#{@assetMap[url]}\"></script>"
181 when '.css'
182 return "\n<link rel=\"stylesheet\" href=\"//#{@hostname}#{@assetMap[url]}\">"
183
184 # Get the hashed url for a given url
185 url: (url) ->
186 return "//#{@hostname}#{@assetMap[url]}"
187
188
189# Shortcut function
190exports.fromConfigFile = (options) ->
191 return new ConfigRack(options)