UNPKG

16 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6
7var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
8
9var _Compiler = require('./compiler/Compiler');
10
11var _Compiler2 = _interopRequireDefault(_Compiler);
12
13var _Router = require('./router/Router');
14
15var _Router2 = _interopRequireDefault(_Router);
16
17var _EntryPoint = require('./EntryPoint');
18
19var _EntryPoint2 = _interopRequireDefault(_EntryPoint);
20
21var _ServerHttpRequest = require('./http/ServerHttpRequest');
22
23var _ServerHttpRequest2 = _interopRequireDefault(_ServerHttpRequest);
24
25var _Provider = require('./Provider.js');
26
27var _Provider2 = _interopRequireDefault(_Provider);
28
29var _CookieJar = require('./http/CookieJar.js');
30
31var _CookieJar2 = _interopRequireDefault(_CookieJar);
32
33var _ServerCookieJar = require('./http/ServerCookieJar.js');
34
35var _ServerCookieJar2 = _interopRequireDefault(_ServerCookieJar);
36
37var _fs = require('fs');
38
39var _fs2 = _interopRequireDefault(_fs);
40
41var _path = require('path');
42
43var _path2 = _interopRequireDefault(_path);
44
45var _http = require('http');
46
47var _http2 = _interopRequireDefault(_http);
48
49var _domain = require('domain');
50
51var _domain2 = _interopRequireDefault(_domain);
52
53function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
54
55function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
56
57/**
58 * Orchestrates all the things that makes the application server run:
59 *
60 * 1. Gets the list of entry points from ComponentRegister.
61 * 2. Compiles the code with the given Compiler implementation.
62 * 3. Create and run the server.
63 * 4. Handles the requests.
64 *
65 * In handling the requests:
66 *
67 * 1. Passes request and response to middlewares generated by Compiler.
68 * 2. If not handled, Compiler middlewares will call next, which will pass
69 * the torch to soya middleware.
70 * 3. Soya middleware will ask Router which page to run, ask Page to render,
71 * ask Compiler to assemble HTML and send response.
72 *
73 * Uses node-js domain for error handling.
74 *
75 * TODO: Make every process async with Promise?
76 *
77 * This object is stateless, should not store ANY request-specific states.
78 *
79 * @SERVER
80 */
81var Application = function () {
82
83 /**
84 * @param {Logger} logger
85 * @param {ComponentRegister} componentRegister
86 * @param {Object} routes
87 * @param {Router} router
88 * @param {Compiler} compiler
89 * @param {ErrorHandler} errorHandler
90 * @param {ReverseRouter} reverseRouter
91 * @param {Object} frameworkConfig
92 * @param {Object} serverConfig
93 * @param {Object} clientConfig
94 */
95
96
97 /**
98 * The idea is to have middleware system that is compatible with express
99 * middlewares. Since express middlewares are just functions accepting req,
100 * res, and next - it should not be hard to make it compatible.
101 * Kudos to the express team to make such an awesome framework btw.
102 *
103 * @type {Array<Function>}
104 */
105
106
107 /**
108 * @type {Provider}
109 */
110
111
112 /**
113 * @type {ErrorHandler}
114 */
115
116
117 /**
118 * @type {ComponentRegister}
119 */
120
121
122 /**
123 * @type {Compiler}
124 */
125
126
127 /**
128 * @type {Object}
129 */
130
131
132 /**
133 * @type {Router}
134 */
135 function Application(logger, componentRegister, routes, router, reverseRouter, errorHandler, compiler, frameworkConfig, serverConfig, clientConfig) {
136 _classCallCheck(this, Application);
137
138 // Change register to real client registration function.
139 this._addReplace(frameworkConfig, 'soya/lib/client/Register', 'soya/lib/client/RegisterClient');
140
141 // Change react renderer to client version.
142 this._addReplace(frameworkConfig, 'soya/lib/page/react/ReactRenderer', 'soya/lib/page/react/ReactRendererClient');
143
144 // Allow users to run code blocks on server or client.
145 this._addReplace(frameworkConfig, 'soya/lib/scope', 'soya/lib/scope-client');
146
147 // Replace custom node registration function for client.
148 if (frameworkConfig.routerNodeRegistrationAbsolutePath) {
149 this._addReplace(frameworkConfig, 'soya/lib/server/registerRouterNodes', frameworkConfig.routerNodeRegistrationAbsolutePath);
150 }
151
152 this._logger = logger;
153 this._serverCreated = false;
154 this._componentRegister = componentRegister;
155 this._compiler = compiler;
156 this._frameworkConfig = frameworkConfig;
157 this._serverConfig = serverConfig;
158 this._clientConfig = clientConfig;
159 this._router = router;
160 this._errorHandler = errorHandler;
161 this._pages = {};
162 this._routeForPages = {};
163 this._entryPoints = [];
164 this._pageClasses = {};
165 this._provider = new _Provider2.default(serverConfig, reverseRouter, true);
166 this._absoluteClientDepFile = _path2.default.join(this._frameworkConfig.absoluteProjectDir, 'build/dep.json');
167
168 var cookieJar = new _CookieJar2.default();
169 var i,
170 pageCmpt,
171 page,
172 pageComponents = componentRegister.getPages();
173 var routeRequirements, j, routeId;
174
175 for (i in pageComponents) {
176 if (!pageComponents.hasOwnProperty(i)) continue;
177 pageCmpt = pageComponents[i];
178
179 // Create entry point.
180 this._entryPoints.push(new _EntryPoint2.default(pageCmpt.name, pageCmpt.absDir));
181 this._pageClasses[pageCmpt.name] = pageCmpt.clazz;
182
183 try {
184 // Instantiate page. We try to instantiate page at startup to find
185 // potential problems with each page. This allows us to detect factory
186 // naming clash early on while also allowing the start-up process to
187 // populate Provider with ready to use dependencies.
188 page = new pageCmpt.clazz(this._provider, cookieJar, true);
189 } catch (e) {
190 throw e;
191 }
192
193 this._routeForPages[pageCmpt.name] = {};
194 if (typeof pageCmpt.clazz.getRouteRequirements == 'function') {
195 routeRequirements = pageCmpt.clazz.getRouteRequirements();
196 for (j = 0; j < routeRequirements.length; j++) {
197 routeId = routeRequirements[j];
198 if (!routes.hasOwnProperty(routeId)) {
199 throw new Error('Page ' + pageCmpt.name + ' has dependencies to unknown route: ' + routeId + '.');
200 }
201 this._routeForPages[pageCmpt.name][routeId] = routes[routeId];
202 }
203 }
204 }
205
206 this._middlewares = [];
207 }
208
209 /**
210 * @param {Object} frameworkConfig
211 * @param {string} source
212 * @param {string} replacement
213 */
214
215
216 /**
217 * @type {boolean}
218 */
219
220
221 /**
222 * @type {{[key: string]: Function}}
223 */
224
225
226 /**
227 * @type {Logger}
228 */
229
230
231 /**
232 * @type {Array<EntryPoint>}
233 */
234
235
236 /**
237 * @type {CompileResult}
238 */
239
240
241 /**
242 * @type {Object}
243 */
244
245
246 /**
247 * @type {Object}
248 */
249
250 /**
251 * @type {Object}
252 */
253
254
255 _createClass(Application, [{
256 key: '_addReplace',
257 value: function _addReplace(frameworkConfig, source, replacement) {
258 frameworkConfig.clientReplace[source] = replacement;
259 frameworkConfig.clientReplace[source + '.js'] = replacement;
260 }
261
262 /**
263 * Compiles and then create an http server that handles requests.
264 */
265
266 }, {
267 key: 'start',
268 value: function start() {
269 var _this = this;
270
271 // If precompileClient true, try get page dependency map from previously generated dep.json to
272 // fill this._compileResult
273 if (this._frameworkConfig.precompileClient && _fs2.default.existsSync(this._absoluteClientDepFile)) {
274 this._middlewares = this._compiler.run(this._entryPoints, null, false);
275 this._compileResult = JSON.parse(_fs2.default.readFileSync(this._absoluteClientDepFile));
276 } else {
277 // Runs runtime compilation. This will update compilation result when
278 // compilation is done, while returning array of compiler specific
279 // middlewares for us.
280 this._middlewares = this._compiler.run(this._entryPoints, function (compileResult) {
281 _this._compileResult = compileResult;
282 });
283 }
284
285 // Add soya middleware as the last one.
286 this._middlewares.push(this.handle.bind(this));
287
288 return this._middlewares;
289 }
290
291 /**
292 * Do mostly the same work as start(), except this doesn't createServer() and write compile result to dep.json
293 */
294
295 }, {
296 key: 'buildClient',
297 value: function buildClient() {
298 var _this2 = this;
299
300 this._compiler.run(this._entryPoints, function (compileResult) {
301 _fs2.default.writeFileSync(_this2._absoluteClientDepFile, JSON.stringify(compileResult), 'utf8');
302 });
303 }
304 }, {
305 key: 'createServer',
306 value: function createServer() {
307 var _this3 = this;
308
309 if (this._serverCreated) {
310 // No need to create more than one server.
311 return;
312 }
313
314 // No need to listen twice.
315 this._serverCreated = true;
316
317 // TODO: Config can set timeout for http requests.
318 _http2.default.createServer(function (request, response) {
319 // Control for favicon
320 if (request.method === 'GET' && request.url === '/favicon.ico') {
321 var faviconPath = _path2.default.join(_this3._frameworkConfig.absoluteProjectDir, 'favicon.ico');
322 if (_fs2.default.existsSync(faviconPath)) {
323 response.writeHead(200, { 'Content-Type': 'image/x-icon' });
324 response.end(_fs2.default.readFileSync(faviconPath), 'binary');
325 return;
326 }
327 }
328 var d = _domain2.default.create().on('error', function (error) {
329 _this3.handleError(error, request, response);
330 });
331 d.run(function () {
332 var index = 0;
333 var runMiddleware = function runMiddleware() {
334 var middleware = _this3._middlewares[index++];
335 if (!middleware) return;
336 middleware(request, response, runMiddleware);
337 };
338
339 // Run the first middleware.
340 runMiddleware();
341 });
342 }).listen(this._frameworkConfig.port, function () {
343 if (process && typeof process.send === 'function') process.send('ready');
344 _this3._logger.info('Server listening at port: ' + _this3._frameworkConfig.port + '.');
345 });
346 }
347
348 /**
349 * @param {http.incomingMessage} request
350 * @param {httpServerResponse} response
351 * @param {?void} next
352 */
353
354 }, {
355 key: 'handle',
356 value: function handle(request, response, next) {
357 // Directly over to next-js when it is next request
358 if (request.url.indexOf("_next") !== -1) {
359 next();
360 return;
361 }
362
363 var httpRequest = new _ServerHttpRequest2.default(request, this._frameworkConfig.maxRequestBodyLength);
364 var routeResult = this._router.route(httpRequest);
365 // Over to next-js when page is not found
366 if (routeResult == null) {
367 next();
368 return;
369 }
370
371 var pageClass = this._pageClasses[routeResult.pageName];
372 if (!pageClass) {
373 throw new Error('Unable to route request, page ' + routeResult.pageName + ' doesn\'t exist');
374 }
375
376 // Because we tried to instantiate all pages at start-up we can be sure
377 // that pageClass exists.
378 var cookieJar = new _ServerCookieJar2.default(request);
379 var page = new pageClass(this._provider, cookieJar, true);
380 var store = page.createStore(null);
381
382 this._logger.debug('Rendering page: ' + routeResult.pageName + '.', null);
383 page.render(httpRequest, routeResult.routeArgs, store, this._handleRenderResult.bind(this, routeResult, request, httpRequest, response, store, cookieJar));
384 }
385
386 /**
387 * @param {RouteResult} routeResult
388 * @param {http.incomingMessage} request
389 * @param {ServerHttpRequest} httpRequest
390 * @param {httpServerResponse} response
391 * @param {void | Store} store
392 * @param {ServerCookieJar} cookieJar
393 * @param {RenderResult} renderResult
394 */
395
396 }, {
397 key: '_handleRenderResult',
398 value: function _handleRenderResult(routeResult, request, httpRequest, response, store, cookieJar, renderResult) {
399 var _this4 = this;
400
401 var pageDep = this._compileResult.pages[routeResult.pageName];
402 if (!pageDep) {
403 throw new Error('Unable to render page server side, dependencies unknown for entry point: ' + routeResult.componentName);
404 }
405
406 var promise = Promise.resolve(null);
407
408 if (store) {
409 if (store._shouldRenderBeforeServerHydration()) {
410 store._startRender();
411 // Render first to let all segment and query requirements registered
412 // to the store. This is weird and sort of wasteful, but we haven't
413 // found a better way yet.
414 renderResult.contentRenderer.render(routeResult.routeArgs, this._routeForPages[routeResult.pageName], this._clientConfig, null, pageDep);
415 store._endRender();
416 }
417
418 //this._logger.debug('Store requirements gathered, start hydration.', null, store);
419 promise = store.hydrate();
420 }
421
422 var handlePromiseError = function handlePromiseError(error) {
423 // Just in case user store code doesn't reject with Error object.
424 error = _this4._ensureError(error);
425 _this4.handleError(error, request, response);
426 };
427
428 var storeResolve = function storeResolve() {
429 var state = null;
430 if (store) {
431 state = store._getState();
432 //this._logger.debug('Finish hydration.', null, state);
433 store._startRender();
434 }
435
436 var htmlResult = renderResult.contentRenderer.render(routeResult.routeArgs, _this4._routeForPages[routeResult.pageName], _this4._clientConfig, state, pageDep);
437
438 if (store) store._endRender();
439
440 response.statusCode = renderResult.httpStatusCode;
441 response.statusMessage = renderResult.httpStatusMessage;
442
443 // TODO: Calculating content length as utf8 is hard-coded. This might be harmful, maybe move as configuration of the compiler?
444 response.setHeader('Content-Length', Buffer.byteLength(htmlResult, 'utf8'));
445 response.setHeader('Content-Type', 'text/html;charset=UTF-8');
446
447 // Set result headers.
448 var key,
449 headerData = renderResult.httpHeaders.getAll();
450 for (key in headerData) {
451 if (!headerData.hasOwnProperty(key)) continue;
452 response.setHeader(key, headerData[key]);
453 }
454
455 // Set result cookies.
456 var cookieValues = cookieJar.generateHeaderValues();
457 if (cookieValues.length > 0) {
458 response.setHeader('Set-Cookie', cookieValues);
459 }
460
461 // Set result content.
462 response.end(htmlResult);
463 };
464
465 promise.then(storeResolve).catch(handlePromiseError);
466 }
467
468 /**
469 * @param {Error} error
470 * @param {http.incomingRequest} request
471 * @param {httpServerResponse} response
472 */
473
474 }, {
475 key: 'handleError',
476 value: function handleError(error, request, response) {
477 if (response.headersSent) {
478 this._errorHandler.responseSentError(error, request, response);
479 return;
480 }
481
482 this._errorHandler.responseNotSentError(error, request, response);
483 }
484
485 /**
486 * @param {any} error
487 * @return {Error}
488 */
489
490 }, {
491 key: '_ensureError',
492 value: function _ensureError(error) {
493 if (error instanceof Error) return error;
494 if (typeof error == 'string') return new Error(error);
495 return new Error('Error when resolving store promise! Unable to convert reject arg: ' + error);
496 }
497 }]);
498
499 return Application;
500}();
501
502exports.default = Application;
503//# sourceMappingURL=Application.js.map
\No newline at end of file