UNPKG

23 kBJavaScriptView Raw
1import 'logger/server'
2import async from 'async'
3import cookie from 'cookie'
4import bodyParser from 'body-parser'
5import cookieParser from 'cookie-parser'
6import express from 'express'
7import InternationalizationHandler from 'i18n/InternationalizationHandler'
8import UniversalPageHandler from 'handler/UniversalPageHandler'
9import path from 'path'
10import preconditions from 'preconditions'
11import querystring from 'querystring'
12import React from 'react'
13import { renderToStaticMarkup } from 'react-dom/server'
14import { Provider } from 'react-redux'
15import { Route, RouterContext, match } from 'react-router'
16import { createStore, combineReducers, applyMiddleware } from 'redux'
17import thunk from 'redux-thunk'
18import SimpleHtmlRenderer from 'render/SimpleHtmlRenderer'
19import { serverFetchReducer } from 'fetch/core'
20import resolve from 'es6-template-strings/resolve-to-string'
21import forceDomain from 'forcedomain'
22import url from 'url'
23import rfs from 'rotating-file-stream'
24import morgan from 'morgan'
25import fs from 'fs'
26import parseUrl from 'parseurl'
27
28let project = __PROJECT__
29let logger = global.Logger.getLogger('ExpressUniversalApplicationServer')
30// Currently supported locale is /{lang}-{country}/*
31let LOCALE_REGEX = /^\/([a-z]{2})-([a-z]{2})(.*)$/
32let LOCALE_DEFAULT = 'id-id'
33
34let QS_REGEX = /^\?(.*)$/
35
36const createStoreWithMiddleware = applyMiddleware(
37 thunk
38)(createStore)
39
40// handle error, so that server won't crash
41process.on('uncaughtException', function (err) {
42 console.log("[CRASH] [ERROR] error log: ", err)
43})
44
45class ExpressUniversalApplicationServer {
46 constructor (options) {
47 let pc = preconditions.instance(options)
48 pc.shouldBeDefined('port', 'port must be defined.')
49 pc.shouldBeDefined('rootApplicationPath', 'rootApplicationPath must be defined.')
50 pc.shouldBeDefined('rootDeploymentApplicationPath', 'rootDeploymentApplicationPath must be defined.')
51 this._options = options
52 this._app = express()
53 this._routes = this.getRoutes()
54 this._reducers = this.getReducers()
55 this._initialize()
56 }
57
58 _initialize () {
59 // this._setupRequestLogMiddleware()
60 this._setupForceDomain()
61 this._setupAssetsServing()
62 this._setupCookieParser()
63 this._setupBodyParser()
64 this._setupHtmlRenderer()
65 this._setupInternationalizedRoutes()
66 this._setupI18nHandler()
67 }
68
69 _setupRequestLogMiddleware () {
70 let logDirectory = '/logs/' + project.applicationName
71 fs.existsSync(logDirectory) || fs.mkdirSync(logDirectory)
72
73 function pad (num) {
74 return (num > 9 ? '' : '0') + num
75 }
76
77 function generator (time, index) {
78 logger.info('[GENERATOR] Request log rotation ' + time + '-' + index)
79 if (!time) {
80 // return request.log
81 }
82
83 var yearMonth = time.getFullYear() + '-' + pad(time.getMonth() + 1)
84 var day = pad(time.getDate())
85 var hour = pad(time.getHours())
86 var minute = pad(time.getMinutes())
87 var seconds = pad(time.getSeconds())
88
89 logger.info('[GENERATOR] ' + logDirectory + '/request-' + yearMonth + '-' + day + '-' + hour + '-' + minute + '-' + seconds + '.log.gz')
90
91 return 'request-' + yearMonth + '-' + day + '-' + hour + '-' + minute + '-' + seconds + 'log.gz'
92 }
93
94 this._accessLogStream = rfs(generator, {
95 path: logDirectory,
96 compress: true,
97 interval: '10s',
98 rotate: 7
99 })
100
101 logger.info('Setting up morgan (Request Log Middleware)')
102 this._app.use(morgan('combined', { stream: this._accessLogStream }))
103 }
104
105 _setupAssetsServing () {
106 console.log(path.join(this._options.rootDeploymentApplicationPath, 'build', this._options.environment, 'client'))
107 // TODO : this should be configurable, but we hard code it for now
108 logger.info('Using ' + path.join(this._options.rootDeploymentApplicationPath, 'build', this._options.environment, 'client') + ', with /assets as assets serving routes')
109 this._app.use('/assets', express.static(path.join(this._options.rootDeploymentApplicationPath, 'build', this._options.environment, 'client')))
110
111 logger.info('Using ' + path.join(this._options.rootDeploymentApplicationPath, 'build', this._options.environment, 'server', 'debugging') + ', with /__dev/assets/server/debugging as server-debugging serving routes')
112 this._app.use('/__dev/assets/server/debugging', express.static(path.join(this._options.rootDeploymentApplicationPath, 'build', this._options.environment, 'server', 'debugging')))
113
114 // TODO : this should be configurable, but we hard code it for now
115 logger.info('Using ' + path.join(this._options.rootDeploymentApplicationPath, 'assets') + ', with /assets/static as static non-compileable assets serving routes')
116 this._app.use('/assets/static', express.static(path.join(this._options.rootDeploymentApplicationPath, 'assets')))
117 }
118
119 _setupCookieParser () {
120 this._app.use(cookieParser())
121 }
122
123 _setupBodyParser () {
124 this._app.use(bodyParser.urlencoded({ extended: true }))
125 this._app.use(bodyParser.json())
126 }
127
128 _setupHtmlRenderer () {
129 let htmlRendererOptions = {
130 path: path.join(this._options.rootDeploymentApplicationPath, 'html'),
131 cache: !(this._options.environment === 'development')
132 }
133 this._renderer = new SimpleHtmlRenderer(htmlRendererOptions)
134 }
135
136 _setupInternationalizedRoutes () {
137 let finalRoutes = null
138 let originalRoutes = this.getRoutes()
139 let generatedRoutes = []
140 this._options.locales.forEach((locale) => {
141 let internationalRoute = (
142 <Route key={locale} path={locale} component={InternationalizationHandler}>
143 {originalRoutes}
144 </Route>
145 )
146 generatedRoutes.push(internationalRoute)
147 })
148
149 generatedRoutes.push(originalRoutes)
150
151 finalRoutes = (
152 <Route path='/' component={UniversalPageHandler}>
153 {
154 generatedRoutes.map((route) => {
155 return route
156 })
157 }
158 </Route>
159 )
160
161 this._routes = finalRoutes
162 }
163
164 _setupI18nHandler () {
165 this._app.use((req, res, next) => {
166 let match = LOCALE_REGEX.exec(req.url)
167 let url = null
168 if (match != null) {
169 let lang = match[1]
170 let country = match[2]
171 url = match[3]
172 req.locale = lang + '-' + country
173 } else {
174 req.locale = LOCALE_DEFAULT
175 }
176 if (this._options.locales.indexOf(req.locale) >= 0) {
177 next()
178 } else {
179 if (this._options.locales.length === 0) {
180 next()
181 } else {
182 if (url == null || typeof url === 'undefined' || url.length === 0) {
183 res.redirect('/')
184 } else {
185 res.redirect(url)
186 }
187 }
188 }
189 })
190 }
191
192 _isIP (host) {
193 let ipRegex = /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/
194 return ipRegex.test(host)
195 }
196
197 _setupForceDomain () {
198 let protocol = 'http'
199 if (this._options.forceHttps === true) {
200 protocol = 'https'
201 }
202 let hostName = this._stripProtocol(this._options.applicationHost)
203 if (!this._isIP(hostName)) {
204 let parsedApplicationHost = url.parse(this._options.applicationHost)
205 if (parsedApplicationHost.port == null) {
206 this._app.use(forceDomain({
207 hostname: parsedApplicationHost.host,
208 protocol: protocol
209 }))
210 }
211 }
212 }
213
214 _stripProtocol (url) {
215 if (url != null && typeof url !== 'undefined') {
216 let result = url.replace(/.*?:\/\//g, '')
217 return result
218 }
219 return null
220 }
221
222 _handleError500 (message, err, res) {
223 logger.error(message, err)
224 if (this._options.environment === 'production') {
225 res.redirect(this.getErrorHandler())
226 } else {
227 let error = '<br />'
228 if (this._options.environment === 'development') {
229 if (err != null && typeof err !== 'undefined') {
230 error += err.stack
231 }
232 }
233 error = error.replace('\n', '<br />')
234 this._renderer.render('500.html', (err, rendered) => {
235 if (err) {
236 logger.error('[ERROR_RENDER_FATAL] An unknown error occurred when trying to render error 500.', err)
237 return res.status(500).end('An unknown error occurred when trying to render error 500\n' + err.stack)
238 } else {
239 return res.status(500).end(resolve(rendered, { ERROR: error }))
240 }
241 })
242 }
243 }
244
245 _handleNotFound404 (res) {
246 this._renderer.render('404.html', (err, rendered) => {
247 if (err) {
248 logger.error('[ERROR_RENDER_FATAL] An unknown error occurred when trying to render error 404.', err)
249 return res.status(500).end('An unknown error occurred when trying to render error 404\n' + err.stack)
250 } else {
251 return res.status(404).end(resolve(rendered, {}))
252 }
253 })
254 }
255
256 _runFilterPreFetchStage (filterQueue, renderProps, renderedServerComponents, req, res, context) {
257 if (filterQueue.length > 0) {
258 let filterFnQueue = []
259 filterQueue.reverse().forEach((filterFn) => {
260 filterFnQueue.push((callback) => {
261 let filterCallContext = {
262 get: function () {
263 return context
264 },
265 next: function (booleanResult) {
266 if (typeof booleanResult === 'undefined' || booleanResult == null) {
267 booleanResult = true
268 }
269 callback(null, booleanResult)
270 },
271 redirect: function (redirect) {
272 callback({ type: 'REDIRECTION', redirect: redirect }, false)
273 },
274 notFound: function () {
275 callback({ type: 'NOT_FOUND' }, false)
276 },
277 abortWithError: function () {
278 callback({ type: 'SERVER_ERROR' }, false)
279 }
280 }
281 filterFn(filterCallContext)
282 })
283 })
284 async.series(filterFnQueue, (err, results) => {
285 if (err) {
286 if (err.type === 'REDIRECTION') {
287 return res.redirect(err.redirect)
288 } else if (err.type === 'NOT_FOUND') {
289 this._renderer.render('404.html', (err, rendered) => {
290 if (err) {
291 logger.error('[ERROR_RENDER_FATAL] An unknown error occurred when trying to render error 404.', err)
292 return res.status(500).end('An unknown error occurred when trying to render error 404\n' + err.stack)
293 } else {
294 return res.status(404).end(resolve(rendered, {}))
295 }
296 })
297 } else if (err.type === 'SERVER_ERROR') {
298 this._renderer.render('500.html', (err, rendered) => {
299 if (err) {
300 logger.error('[ERROR_RENDER_FATAL] An unknown error occurred when trying to render error 500.', err)
301 return res.status(500).end('An unknown error occurred when trying to render error 500\n' + err.stack)
302 } else {
303 return res.status(500).end(resolve(rendered, {}))
304 }
305 })
306 } else {
307 this._renderer.render('500.html', (err, rendered) => {
308 if (err) {
309 logger.error('[ERROR_RENDER_FATAL] An unknown error occurred when trying to render error 500.', err)
310 return res.status(500).end('An unknown error occurred when trying to render error 500\n' + err.stack)
311 } else {
312 return res.status(500).end(resolve(rendered, {}))
313 }
314 })
315 }
316 } else {
317 this._runFetchStage(
318 renderProps,
319 renderedServerComponents,
320 req,
321 res,
322 context)
323 }
324 })
325 } else {
326 this._runFetchStage(
327 renderProps,
328 renderedServerComponents,
329 req,
330 res,
331 context)
332 }
333 }
334
335 _runFetchStage (renderProps, renderedServerComponents, req, res, context) {
336 let fetchDataQueue = []
337 let dataContextObject = {}
338
339 renderedServerComponents.forEach((rsc) => {
340 if (rsc.__fetchData != null && typeof rsc.__fetchData !== 'undefined') {
341 let fnFetchCall = (callback) => {
342 setTimeout(() => {
343 rsc.__fetchData(dataContextObject, context, callback)
344 }, 1)
345 }
346 fetchDataQueue.push(fnFetchCall)
347 }
348 })
349
350 let reducers = this._reducers
351
352 async.series(fetchDataQueue, (err, results) => {
353 if (err) {
354 return this._handleError500('[FETCH_DATA_ERROR] An unknown error occurred when trying to fetch data.', err, res)
355 } else {
356 let fetchedDataContext = {}
357 results.forEach((result) => {
358 fetchedDataContext = {
359 ...fetchedDataContext,
360 ...result
361 }
362 })
363
364 let allReducers = {
365 ...reducers,
366 view: serverFetchReducer
367 }
368 const store = createStoreWithMiddleware(combineReducers(allReducers))
369
370 store.dispatch({
371 type: '__SET_VIEW_STATE__',
372 data: fetchedDataContext
373 })
374
375 const InitialComponent = (
376 <Provider store={store}>
377 <RouterContext {...renderProps} />
378 </Provider>
379 )
380
381 let postFetchFilterQueue = []
382
383 renderedServerComponents.forEach((rsc) => {
384 if (rsc.__postFetchFilter != null && typeof rsc.__postFetchFilter !== 'undefined') {
385 postFetchFilterQueue = postFetchFilterQueue.concat(rsc.__postFetchFilter)
386 }
387 })
388 this._runFilterPostFetchStage(
389 postFetchFilterQueue,
390 store,
391 InitialComponent,
392 renderedServerComponents,
393 req,
394 res,
395 context)
396 }
397 })
398 }
399
400 _runFilterPostFetchStage (filterQueue, store, InitialComponent, renderedServerComponents, req, res, context) {
401 if (filterQueue.length > 0) {
402 let filterFnQueue = []
403 filterQueue.reverse().forEach((filterFn) => {
404 filterFnQueue.push((callback) => {
405 let filterCallContext = {
406 store: function () {
407 return store.getState().view
408 },
409 get: function () {
410 return context
411 },
412 next: function (booleanResult) {
413 if (typeof booleanResult === 'undefined' || booleanResult == null) {
414 booleanResult = true
415 }
416 callback(null, booleanResult)
417 },
418 redirect: function (redirect) {
419 callback({ type: 'REDIRECTION', redirect: redirect }, false)
420 },
421 notFound: function () {
422 callback({ type: 'NOT_FOUND' }, false)
423 }
424 }
425 filterFn(filterCallContext)
426 })
427 })
428 async.series(filterFnQueue, (err, results) => {
429 if (err) {
430 if (err.type === 'REDIRECTION') {
431 return res.redirect(err.redirect)
432 } else if (err.type === 'NOT_FOUND') {
433 this._renderer.render('404.html', (err, rendered) => {
434 if (err) {
435 logger.error('[ERROR_RENDER_FATAL] An unknown error occurred when trying to render error 404.', err)
436 return res.status(500).end('An unknown error occurred when trying to render error 404\n' + err.stack)
437 } else {
438 return res.status(404).end(resolve(rendered, {}))
439 }
440 })
441 } else {
442 // TODO what case need to be handled?
443 }
444 } else {
445 this._runRenderStage(
446 store,
447 InitialComponent,
448 renderedServerComponents,
449 req,
450 res,
451 context)
452 }
453 })
454 } else {
455 this._runRenderStage(
456 store,
457 InitialComponent,
458 renderedServerComponents,
459 req,
460 res,
461 context)
462 }
463 }
464
465 _createCookieSerializationOption (header) {
466 let option = {
467 path: header.path,
468 domain: header.domain,
469 version: header.version
470 }
471
472 if (header.maxAge > 0) {
473 option.maxAge = header.maxAge
474 }
475
476 return option
477 }
478
479 _runSendResponseStage (res, PAGE_HTML, context) {
480 let responseCookies = context.response.cookies
481 let responseHeaders = context.response.headers.cookies
482
483 Object.keys(responseCookies).forEach((krc) => {
484 let __cookie = responseCookies[krc]
485 let __cookieHeader = {}
486 if (responseHeaders != null && typeof responseHeaders !== 'undefined') {
487 __cookieHeader = responseHeaders[krc]
488 }
489 if (__cookie != null && typeof __cookie !== 'undefined') {
490 let serializedCookie = cookie.serialize(krc, __cookie, this._createCookieSerializationOption(__cookieHeader))
491 res.append('Set-Cookie', serializedCookie)
492 }
493 })
494 res.end(PAGE_HTML)
495 }
496
497 _runRenderStage (store, InitialComponent, renderedServerComponents, req, res, context) {
498 let finalRenderPage = 'main.html'
499 renderedServerComponents.forEach((rsc) => {
500 if (rsc.__renderPage != null && typeof rsc.__renderPage !== 'undefined') {
501 finalRenderPage = rsc.__renderPage
502 }
503 })
504 let renderBindFnQueue = []
505 renderedServerComponents.forEach((rsc) => {
506 if (rsc.__renderBindFn != null && typeof rsc.__renderBindFn !== 'undefined') {
507 let bindFnCall = (callback) => {
508 setTimeout(() => {
509 rsc.__renderBindFn(store.getState(), context, callback)
510 }, 1)
511 }
512 renderBindFnQueue.push(bindFnCall)
513 }
514 })
515
516 if (renderBindFnQueue.length > 0) {
517 async.series(renderBindFnQueue, (err, results) => {
518 if (err) {
519 return this._handleError500('[RENDER_BIND_PHASE] FATAL_ERROR in render data binding phase.', err, res)
520 } else {
521 this._renderer.render(finalRenderPage, (err, rendered) => {
522 if (err) {
523 return this._handleError500('[RENDER_VIEW_PHASE] FATAL_ERROR in render view template phase.', err, res)
524 } else {
525 let bindData = {}
526 if (results != null && typeof results !== 'undefined') {
527 results.forEach((r) => {
528 bindData = {
529 ...bindData,
530 ...r
531 }
532 })
533 }
534 try {
535 const HTML = renderToStaticMarkup(InitialComponent)
536 const PAGE_HTML = resolve(rendered, {
537 ...bindData,
538 HTML: HTML,
539 DATA: store.getState()
540 }, { partial: true })
541 return this._runSendResponseStage(res, PAGE_HTML, context)
542 } catch (err) {
543 return this._handleError500('[RENDER_STATIC_MARKUP_PHASE] FATAL_ERROR in render staticMarkup phase.', err, res)
544 }
545 }
546 })
547 }
548 })
549 } else {
550 this._renderer.render(finalRenderPage, (err, rendered) => {
551 if (err) {
552 return this._handleError500('[RENDER_VIEW_PHASE] FATAL_ERROR in render view template phase.', err, res)
553 } else {
554 let bindData = {}
555 try {
556 const HTML = renderToStaticMarkup(InitialComponent)
557 const PAGE_HTML = resolve(rendered, {
558 ...bindData,
559 HTML: HTML,
560 DATA: store.getState()
561 }, { partial: true })
562 return this._runSendResponseStage(res, PAGE_HTML, context)
563 } catch (err) {
564 return this._handleError500('[RENDER_STATIC_MARKUP_PHASE] FATAL_ERROR in render staticMarkup phase.', err, res)
565 }
566 }
567 })
568 }
569 }
570
571 _importRequestCookie (context, req) {
572 let cookies = req.cookies
573 Object.keys(cookies).forEach((ck) => {
574 context.request.cookies[ck] = req.cookies[ck]
575 })
576 }
577
578 _handleHealthCheck(res) {
579 return res.status(200).send('ok')
580 }
581
582 _setupRoutingHandler () {
583 const routes = this._routes
584 this._app.use((req, res) => {
585 const location = req.url
586 const pathname = parseUrl(req).pathname
587 const locale = req.locale
588
589 if (req.url == '/healthz') {
590 return this._handleHealthCheck(res)
591 }
592
593 logger.info('[HANDLING_ROUTE] path: ' + req.url + ', locale: ' + locale)
594 match({ routes, location }, (err, redirectLocation, renderProps) => {
595 if (err) {
596 return this._handleError500('[MATCH_ROUTE_FATAL_ERROR] An unknown error occurred when trying to match routes.', err, res)
597 }
598
599 if (!renderProps) {
600 logger.info('[ROUTE_NOT_FOUND] path: ' + req.url + ', locale: ' + locale)
601 return this._handleNotFound404(res)
602 }
603
604 let query = {}
605 let queryStringMatch = QS_REGEX.exec(renderProps.location.search)
606 let queryString = ''
607 if (queryStringMatch != null && typeof queryStringMatch !== 'undefined') {
608 queryString = queryStringMatch[1]
609 }
610 if (queryString != null && typeof queryString !== 'undefined') {
611 query = querystring.parse(queryString)
612 }
613
614 let simplifiedRoutes = {
615 name: renderProps.routes[renderProps.routes.length - 1].name,
616 path: renderProps.routes[renderProps.routes.length - 1].path
617 }
618
619 let context = {
620 host: this._options.applicationHost,
621 url: req.url,
622 path: pathname,
623 locale: locale,
624 params: renderProps.params,
625 query: query,
626 routes: simplifiedRoutes,
627 environment: this._options.environment,
628 server: true,
629 client: false,
630 useragent: req.useragent,
631 request: {
632 cookies: {},
633 headers: {}
634 },
635 response: {
636 cookies: {},
637 headers: {}
638 }
639 }
640
641 this._importRequestCookie(context, req)
642
643 let renderedServerComponents = renderProps.components
644 let preFetchFilterQueue = []
645
646 renderedServerComponents.forEach((rsc) => {
647 if (rsc.__preFetchFilter != null && typeof rsc.__preFetchFilter !== 'undefined') {
648 preFetchFilterQueue = preFetchFilterQueue.concat(rsc.__preFetchFilter)
649 }
650 })
651
652 this._runFilterPreFetchStage(
653 preFetchFilterQueue,
654 renderProps,
655 renderedServerComponents,
656 req,
657 res,
658 context
659 )
660 })
661 })
662 }
663
664 app () {
665 return this._app
666 }
667
668 getErrorHandler () {
669 return '/'
670 }
671
672 run () {
673 this._setupRoutingHandler()
674 this._app.listen(this._options.port, (err) => {
675 if (err) logger.error(err)
676 else {
677 logger.info('[EXPRESS_UNIVERSAL_APPLICATION_SERVER] Server listening on port : ' + this._options.port)
678 }
679 })
680
681 let pingApp = express()
682 pingApp.use('/ping', (req, res) => {
683 res.end('pong')
684 })
685 pingApp.listen(this._options.pingPort, (err) => {
686 if (err) logger.error(err)
687 else {
688 logger.info('[EXPRESS_UNIVERSAL_APPLICATION_SERVER] Ping server listening on port : ' + this._options.pingPort)
689 }
690 })
691 }
692}
693
694export default ExpressUniversalApplicationServer