UNPKG

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