UNPKG

6.53 kBJavaScriptView Raw
1const React = require('react');
2const { readdirSync } = require('fs');
3const { resolve } = require('path');
4const { renderRoutes, matchRoutes } = require('react-router-config');
5const {
6 isObject,
7 isFunction,
8 isString,
9 isArray,
10 isBoolean,
11 arrayHasValues,
12 resolveComponent,
13 renderComponent,
14 avoidXSS,
15 objectHasValues,
16 getComponentByPathname,
17 getComponentFromRoutes,
18} = require('./helpers.js');
19
20module.exports = (options) => {
21 if (isObject(options)) {
22
23 // Get variables from options (if they were passed...)
24 let { templateHTML, mountId, componentsPath } = options, routes = false, extract = false;
25
26 // Check if routes option is valid:
27 if (('routes' in options)) {
28 if (!isObject(options.routes)) {
29 throw '"routes" property must be an object.';
30 } else {
31 if ('collection' in options.routes) {
32 if (!isArray(options.routes.collection)) {
33 throw '"collection" property must be an array type.';
34 }
35 } else {
36 throw '"routes" property must have a "collection" property.';
37 }
38
39 if ('extractComponent' in options.routes) {
40 if (!isBoolean(options.routes.extractComponent)) {
41 throw '"extractComponent" property must be a boolean type.';
42 } else {
43 extract = options.routes.extractComponent;
44 }
45 } else {
46 extract = false;
47 }
48 }
49
50 // if (!isArray(options.routes)) {
51 // throw new Error('"routes" property must be an array.');
52 // }
53
54 if (arrayHasValues(options.routes.collection)) {
55 routes = true;
56 }
57 }
58
59 // This option is used independently if routes were found or not.
60 if (!templateHTML) {
61 throw '"templateHTML" property must be defined';
62 } else {
63 if (!isString(templateHTML)) {
64 throw '"templateHTML" must be a string path type';
65 }
66 }
67
68 // This option is used independently if routes were found or not.
69 if (!mountId) {
70 throw '"mountId" property must be defined';
71 } else {
72 if (!isString(mountId)) {
73 throw '"mountId" must be a string path type';
74 } else {
75 if (!templateHTML.includes(`id="${mountId}"`)) {
76 throw '"mountId" was not found in the template';
77 }
78 }
79 }
80
81 // Check if componentsPath option is valid (only if routes were not found):
82 if (!routes) {
83 // Prepare component in case routes weren't provided in options.
84 if (!componentsPath) {
85 throw '"componentsPath" property must be defined';
86 } else {
87 if (!isString(componentsPath)) {
88 throw '"componentsPath" must be a string path type';
89 } else {
90 try {
91 readdirSync(componentsPath, { encoding: 'UTF-8' });
92 } catch(err) {
93 throw `\n\nReason: Directory doesn't exists in the filesystem.\ncomponentsPath: "${componentsPath}"\nCode: "${err.code}"\n`;
94 }
95 }
96 }
97 }
98
99 // Prepare the middleware:
100 function middleware(req, res, next) {
101
102 function prepareComponent() {
103 if (routes) {
104 let props = { title: 'Untitled' };
105
106 if (arguments[0]) {
107 if (isObject(arguments[0])) {
108 props = Object.assign(props, arguments[0]);
109 }
110 }
111
112 let results = getComponentFromRoutes(options.routes.collection, req.url, props, extract);
113 results.reactRouter = true;
114
115 return { component: results.Component, props: results };
116 } else {
117 if (arguments[0]) {
118 if (isString(arguments[0])) {
119 /* Require the component: */
120 let component = resolveComponent(resolve(componentsPath, arguments[0]));
121
122 if (component) {
123 let props = { title: 'Untitled' };
124
125 if (arguments.length === 3 && isFunction(arguments[arguments.length-1])) {
126 if (arguments[1]) {
127 if (isObject(arguments[1])) {
128 props = Object.assign(props, arguments[1]);
129 }
130 }
131 }
132
133 return { component, props: { reactRouter: false, props: props } };
134 } else {
135 throw 'component was not found in the filesystem';
136 }
137 } else {
138 throw 'component argument must be a string type';
139 }
140 } else {
141 throw 'component argument must be defined';
142 }
143 }
144 }
145
146 function prepareContent(url, component, props, template, id) {
147 // -------------------------------------------------------- Content:
148 let content = renderComponent(url, component, props);
149 let $ = require('cheerio').load(template);
150 $('title').text(props.props.title);
151 $('head').append(`<script id="__initial_state__">window.__INITIAL_STATE__ = ${avoidXSS(props)};</script>`);
152 $(`#${id}`).html(content.html);
153
154 // -------------------------------------------------------- Return:
155 return {
156 html: $.html(),
157 context: content.context,
158 component: {
159 original: component,
160 rendered: content.html,
161 },
162 props: {
163 original: props.props,
164 stringify: avoidXSS(props.props)
165 },
166 template: template,
167 changes: {
168 title: $('title').html(),
169 state: $('#__initial_state__').html(),
170 mount: $(`#${id}`).html()
171 },
172 };
173 }
174
175 function prepareResults(results, callback) {
176 if (isFunction(callback)) {
177 return callback(results);
178 } else {
179 return results
180 }
181 }
182
183 // Description: Add a new function called "render" to the req object:
184 req.render = function render() {
185 // ---------------------------------------------------------- Component & Props:
186 let { component, props } = prepareComponent(...arguments);
187
188 // ---------------------------------------------------------- Content:
189 let results = prepareContent(req.url, component, props, templateHTML, mountId);
190
191 // ---------------------------------------------------------- Return:
192 return prepareResults(results, arguments[arguments.length-1]);
193 };
194
195 // Call next:
196 return next();
197 };
198
199 // Return the middleware:
200 return middleware;
201 } else {
202 throw 'Options object was not passed to the middleware.'
203 }
204}