1 | import React from 'react';
|
2 | import ReactDOM from 'react-dom/server';
|
3 |
|
4 | import Koa from 'koa';
|
5 | import staticFiles from 'koa-static';
|
6 | import body from 'koa-bodyparser';
|
7 | import compress from 'koa-compress';
|
8 | import session from 'koa-session';
|
9 | import conditionalGet from 'koa-conditional-get';
|
10 | import favicon from 'koa-favicon';
|
11 | import etag from 'koa-etag';
|
12 | import csrf from 'koa-csrf';
|
13 | import uuid from 'node-uuid';
|
14 | import convert from 'koa-convert';
|
15 |
|
16 | import { App as Horse } from 'horse';
|
17 | import Chariot from './app';
|
18 |
|
19 | const App = Chariot(Horse);
|
20 |
|
21 | export function requestGUID () {
|
22 | return async function (ctx, next) {
|
23 | ctx.guid = uuid.v4();
|
24 | await next();
|
25 | };
|
26 | }
|
27 |
|
28 | export function setServerContextProps(server) {
|
29 | return async function (ctx, next) {
|
30 | ctx.synchronous = true;
|
31 | ctx.includeLayout = true;
|
32 |
|
33 | ctx.props = {
|
34 | config: server.app.config,
|
35 | };
|
36 |
|
37 | await next();
|
38 | };
|
39 | }
|
40 |
|
41 | export const MIDDLEWARE_MAP = {
|
42 | staticFiles,
|
43 | compress,
|
44 | session,
|
45 | conditionalGet,
|
46 | favicon,
|
47 | etag,
|
48 | csrf,
|
49 | requestGUID,
|
50 | body,
|
51 | };
|
52 |
|
53 | export function injectBootstrap(ctx, format) {
|
54 | let p = { ...ctx.props };
|
55 |
|
56 | if (format) {
|
57 | p = format({...ctx.props});
|
58 | }
|
59 |
|
60 | delete p.app;
|
61 | delete p.api;
|
62 | delete p.manifest;
|
63 | delete p.dataPromises;
|
64 |
|
65 | const bootstrap = safeStringify(p);
|
66 |
|
67 | const body = ctx.body;
|
68 |
|
69 | if (body && body.lastIndexOf) {
|
70 | const bodyIndex = body.lastIndexOf('</body>');
|
71 | const template = `<script>window.bootstrap=${bootstrap}</script>`;
|
72 | ctx.body = body.slice(0, bodyIndex) + template + body.slice(bodyIndex);
|
73 | }
|
74 | }
|
75 |
|
76 | export function safeStringify (obj) {
|
77 | return JSON.stringify(obj)
|
78 | .replace(/&/g, '\\u0026')
|
79 | .replace(/</g, '\\u003C')
|
80 | .replace(/>/g, '\\u003E');
|
81 | }
|
82 |
|
83 | const GeneratorFunction = Object.getPrototypeOf(eval("(function*(){})")).constructor;
|
84 |
|
85 | export default class Server {
|
86 | constructor (serverConfig, appConfig) {
|
87 | this.config = serverConfig;
|
88 |
|
89 | this.middleware = serverConfig.middleware || [];
|
90 | this.middleware.push(setServerContextProps(this));
|
91 |
|
92 | this.app = new App(appConfig);
|
93 |
|
94 | this.warn(serverConfig);
|
95 |
|
96 | this.server = new Koa();
|
97 | this.server.keys = this.config.keys;
|
98 | }
|
99 |
|
100 | warn (serverConfig) {
|
101 | if (!serverConfig.keys) {
|
102 | console.log('No `keys` passed into serverConfig; your sessions are insecure.');
|
103 | }
|
104 | }
|
105 |
|
106 | enableMiddleware(middleware) {
|
107 | this.middleware.push(middleware);
|
108 | }
|
109 |
|
110 | loadRoutes(routes) {
|
111 | routes(this.app);
|
112 | }
|
113 |
|
114 | async render (ctx) {
|
115 |
|
116 | ctx.type = 'text/html; charset=utf-8';
|
117 |
|
118 | try {
|
119 | if (React.isValidElement(ctx.body)) {
|
120 | let body = ReactDOM.renderToStaticMarkup(ctx.body);
|
121 | ctx.body = body;
|
122 | }
|
123 | } catch (e) {
|
124 | ctx.props.app.error(e, ctx, ctx.props.app);
|
125 | await this.render(ctx);
|
126 | }
|
127 | }
|
128 |
|
129 | serverRender (app) {
|
130 | return async (ctx, next) => {
|
131 | ctx.timings = {};
|
132 |
|
133 | if (ctx.accepts('html')) {
|
134 | const routeStart = Date.now();
|
135 | await app.route(ctx, next);
|
136 | ctx.timings.route = Date.now() - routeStart;
|
137 | }
|
138 |
|
139 | const renderStart = Date.now();
|
140 | await this.render(ctx);
|
141 | ctx.timings.render = Date.now() - renderStart;
|
142 |
|
143 | await injectBootstrap(ctx, this.config.formatBootstrap);
|
144 | }
|
145 | }
|
146 |
|
147 | start() {
|
148 | if (this.started) {
|
149 | throw new Error('Attempted to run `start` twice on a server instance');
|
150 | }
|
151 |
|
152 | this.middleware.forEach((m) => {
|
153 | let middleware = m;
|
154 |
|
155 | if (m instanceof GeneratorFunction) {
|
156 | middleware = convert(m);
|
157 | }
|
158 |
|
159 | this.server.use(middleware);
|
160 | });
|
161 |
|
162 | this.server.use(this.serverRender(this.app));
|
163 | this.server.listen(this.config.port);
|
164 |
|
165 | this.started = true;
|
166 | console.log(`Server istening on ${this.config.port}`);
|
167 | }
|
168 | }
|