UNPKG

8.01 kBJavaScriptView Raw
1var EventEmitter = require('events').EventEmitter
2var gzipMaybe = require('http-gzip-maybe')
3var gzipSize = require('gzip-size')
4var assert = require('assert')
5var path = require('path')
6var pump = require('pump')
7var send = require('send')
8
9var Router = require('./lib/regex-router')
10var ui = require('./lib/ui')
11var bankai = require('./')
12
13var files = [
14 'assets',
15 'documents',
16 'scripts',
17 'manifest',
18 'styles',
19 'service-worker'
20]
21
22module.exports = start
23
24function start (entry, opts) {
25 opts = opts || {}
26
27 assert.equal(typeof entry, 'string', 'bankai/http: entry should be type string')
28 assert.equal(typeof opts, 'object', 'bankai/http: opts should be type object')
29
30 var quiet = !!opts.quiet
31 opts = Object.assign({ reload: true }, opts)
32 var compiler = bankai(entry, opts)
33 var router = new Router()
34 var emitter = new EventEmitter()
35 var id = 0
36 var state = {
37 count: compiler.metadata.count,
38 files: {},
39 sse: 0,
40 size: 0
41 }
42
43 files.forEach(function (filename) {
44 state.files[filename] = {
45 name: filename,
46 progress: 0,
47 timestamp: ' ',
48 size: 0,
49 status: 'pending',
50 done: false
51 }
52 })
53
54 if (!quiet) var render = ui(state)
55 compiler.on('error', function (topic, sub, err) {
56 if (err.pretty) state.error = err.pretty
57 else state.error = `${topic}:${sub} ${err.message}\n${err.stack}`
58 if (!quiet) render()
59 })
60
61 compiler.on('progress', function () {
62 state.error = null
63 if (!quiet) render()
64 })
65
66 compiler.on('ssr', function (result) {
67 state.ssr = result
68 })
69
70 compiler.on('change', function (nodeName, edgeName, nodeState) {
71 var node = nodeState[nodeName][edgeName]
72 var name = nodeName + ':' + edgeName
73 var data = {
74 name: nodeName,
75 progress: 100,
76 timestamp: time(),
77 size: 0,
78 status: 'done',
79 done: true
80 }
81 state.files[nodeName] = data
82
83 if (name === 'documents:index.html') emitter.emit('documents:index.html', node)
84 if (name === 'styles:bundle') emitter.emit('styles:bundle', node)
85
86 // Only calculate the gzip size if there's a buffer. Apparently zipping
87 // an empty file means it'll pop out with a 20B base size.
88 if (node.buffer.length) {
89 gzipSize(node.buffer)
90 .then(function (size) { data.size = size })
91 .catch(function () { data.size = node.buffer.length })
92 .then(function () {
93 if (!quiet) render()
94 })
95 }
96 if (!quiet) render()
97 })
98
99 router.route(/^\/manifest.json$/, function (req, res, params) {
100 compiler.manifest(function (err, node) {
101 if (err) {
102 res.statusCode = 404
103 return res.end(err.message)
104 }
105 res.setHeader('content-type', 'application/json')
106 gzip(node.buffer, req, res)
107 })
108 })
109
110 router.route(/\/(service-worker\.js)|(\/sw\.js)$/, function (req, res, params) {
111 compiler.serviceWorker(function (err, node) {
112 if (err) {
113 res.statusCode = 404
114 return res.end(err.message)
115 }
116 res.setHeader('content-type', 'application/javascript')
117 gzip(node.buffer, req, res)
118 })
119 })
120
121 router.route(/^\/assets\/([^?]*)(\?.*)?$/, function (req, res, params) {
122 var prefix = 'assets' // TODO: also accept 'content'
123 var name = prefix + '/' + params[1]
124 compiler.assets(name, function (err, filename) {
125 if (err) {
126 res.statusCode = 404
127 return res.end(err.message)
128 }
129 pump(send(req, filename), res)
130 })
131 })
132
133 router.route(/\/([a-zA-Z0-9-_.]+)\.js(\?.*)?$/, function (req, res, params) {
134 var name = params[1]
135 compiler.scripts(name, function (err, node) {
136 if (err) {
137 res.statusCode = 404
138 return res.end(err.message)
139 }
140 res.setHeader('content-type', 'application/javascript')
141 gzip(node.buffer, req, res)
142 })
143 })
144
145 router.route(/\/([a-zA-Z0-9-_.]+)\.css(\?.*)?$/, function (req, res, params) {
146 var name = params[1]
147 compiler.styles(name, function (err, node) {
148 if (err) {
149 res.statusCode = 404
150 return res.end(err.message)
151 }
152 res.setHeader('content-type', 'text/css')
153 gzip(node.buffer, req, res)
154 })
155 })
156
157 // Source maps. Each source map is stored as 'foo.js.map' within their
158 // respective node. So in order to figure out the right source map we must
159 // derive figure out where the extension comes from.
160 router.route(/\/([a-zA-Z0-9-_.]+)\.map$/, function (req, res, params) {
161 var source = params[1]
162 var ext = path.extname(source.replace(/\.map$/, ''))
163 var type = source === 'bankai-reload.js'
164 ? 'reload'
165 : source === 'bankai-service-worker.js'
166 ? 'service-worker'
167 : ext === '.js'
168 ? 'scripts'
169 : 'unknown'
170 compiler.sourceMaps(type, source, function (err, node) {
171 if (err) {
172 res.statusCode = 404
173 return res.end(err.message)
174 }
175 res.setHeader('content-type', 'application/json')
176 gzip(node.buffer, req, res)
177 })
178 })
179
180 router.route(/\/reload/, function sse (req, res) {
181 var connected = true
182 emitter.on('documents:index.html', reloadScript)
183 emitter.on('styles:bundle', reloadStyle)
184 state.sse += 1
185 if (!quiet) render()
186
187 res.writeHead(200, {
188 'Content-Type': 'text/event-stream',
189 'X-Accel-Buffering': 'no',
190 'Cache-Control': 'no-cache'
191 })
192 res.write('retry: 10000\n')
193
194 var interval = setInterval(function () {
195 if (res.finished) return // prevent writes after stream has closed
196 res.write(`id:${id++}\ndata:{ "type:": "heartbeat" }\n\n`)
197 }, 4000)
198
199 req.on('error', disconnect)
200 res.on('error', disconnect)
201 res.on('close', disconnect)
202 res.on('finish', disconnect)
203
204 function disconnect () {
205 clearInterval(interval)
206 if (connected) {
207 emitter.removeListener('documents:index.html', reloadScript)
208 emitter.removeListener('styles:bundle', reloadStyle)
209 connected = false
210 state.sse -= 1
211 if (!quiet) render()
212 }
213 }
214
215 function reloadScript (node) {
216 var msg = JSON.stringify({ type: 'scripts' })
217 res.write(`id:${id++}\ndata:${msg}\n\n`)
218 }
219
220 function reloadStyle (node) {
221 var msg = JSON.stringify({
222 type: 'styles',
223 bundle: node.buffer.toString()
224 })
225 res.write(`id:${id++}\ndata:${msg}\n\n`)
226 }
227 })
228
229 router.default(function (req, res, next) {
230 var url = req.url
231
232 if (state.ssr && state.ssr.renderRoute) {
233 state.ssr.renderRoute(url, function (err, buffer) {
234 if (err) {
235 state.ssr.success = false
236 state.ssr.error = err
237 return sendDocument(url, req, res, next)
238 }
239
240 res.setHeader('content-type', 'text/html')
241 gzip(buffer, req, res)
242 })
243 } else {
244 return sendDocument(url, req, res, next)
245 }
246 })
247
248 function sendDocument (url, req, res, next) {
249 compiler.documents(url, function (err, node) {
250 if (err) {
251 return compiler.documents('/404', function (err, node) {
252 if (err) return next() // No matches found, call next
253 res.statusCode = 404
254 res.setHeader('content-type', 'text/html')
255 gzip(node.buffer, req, res)
256 })
257 }
258 res.setHeader('content-type', 'text/html')
259 gzip(node.buffer, req, res)
260 })
261 }
262
263 // TODO: move all UI code out of this file
264 handler.state = state
265 // Expose compiler so we can use it in `bankai start`
266 handler.compiler = compiler
267 return handler
268
269 // Return a handler to listen.
270 function handler (req, res, next) {
271 router.match(req, res, next)
272 }
273}
274
275function gzip (buffer, req, res) {
276 var zipper = gzipMaybe(req, res)
277 pump(zipper, res)
278 zipper.end(buffer)
279}
280
281function time () {
282 var date = new Date()
283 var hours = numPad(date.getHours())
284 var minutes = numPad(date.getMinutes())
285 var seconds = numPad(date.getSeconds())
286 return `${hours}:${minutes}:${seconds}`
287}
288
289function numPad (num) {
290 if (num < 10) num = '0' + num
291 return num
292}