prerenderer.tsx 7.04 KB
Newer Older
Maxym Borodenko's avatar
Maxym Borodenko committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import * as React from 'react';
import { renderToString } from 'react-dom/server';
import createMemoryHistory from 'history/createMemoryHistory';
import { StaticRouter } from 'react-router-dom';
import { Provider as ReduxProvider } from 'react-redux';
import * as serialize from 'serialize-javascript';
import { routerMiddleware } from 'react-router-redux';
import { createStore, applyMiddleware } from 'redux';
import { I18nextProvider } from 'react-i18next';
import thunk from 'redux-thunk';
import rootReducer from 'reducers';

// import { create as createJss } from 'jss';
// import jssPreset from 'jss-preset-default';
import { JssProvider, SheetsRegistry } from 'react-jss';
Matija Obreza's avatar
Matija Obreza committed
16
17
import createGenerateClassName from '@material-ui/core/styles/createGenerateClassName';
import { MuiThemeProvider } from '@material-ui/core/styles';
Maxym Borodenko's avatar
Maxym Borodenko committed
18
19
20
21
22
23
24
25
26
import theme from 'ui/theme';

// import checkAuthToken from './checkAuthToken';
import routes from 'ui/routes';
import matchRoutes from 'ui/matchRoutes';
import renderRoutes from 'ui/renderRoutes';
import fetchComponentData from './fetchComponentData';
import detectLocaleFromPath from './detectLocaleFromPath';
import getDir from './detectDirection';
27
import config from '../config';
Maxym Borodenko's avatar
Maxym Borodenko committed
28
import checkAuthToken from './checkAuthToken';
29
import { configure } from 'actions/applicationConfig';
Maxym Borodenko's avatar
Maxym Borodenko committed
30

Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
31
const prerenderer = (html, errHtml) => (req, res) => {
Maxym Borodenko's avatar
Maxym Borodenko committed
32
33
34
35
36
    console.log('Init prerenderer, request url:', req.url);
    const startTime = Date.now();
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Headers', 'X-Requested-With');
    const store = createStore(rootReducer, {} as any, applyMiddleware(thunk, routerMiddleware(createMemoryHistory)));
37
38
    // Only send public data to client
    store.dispatch(configure({ frontendPath: config.frontendPath, apiUrl: config.apiUrl, googleClientId: config.googleClientId }));
39

40
    console.log(`Processing request`, req._parsedOriginalUrl);
Maxym Borodenko's avatar
Maxym Borodenko committed
41
42
43
44
45
    const pathname = req._parsedOriginalUrl.pathname;
    const search = req._parsedOriginalUrl.search;
    const context = {};

    function setLocale() {
46
        const locale = detectLocaleFromPath(config.frontendPath, pathname, 0);
Maxym Borodenko's avatar
Maxym Borodenko committed
47
48
49
50
51
52
53
54
55
56
        console.log('Detected locale for SSR is', locale);
        req.i18n.changeLanguage(locale);
    }

    function renderView() {
        // const jss = createJss(jssPreset());
        const sheets = new SheetsRegistry();
        const generateClassName = createGenerateClassName();
        const initialLanguage = req.i18n.language;
        const initialI18nStore = {};
Matija Obreza's avatar
Matija Obreza committed
57
        const defaultLanguage = 'en';
58
        initialI18nStore[initialLanguage] = req.i18n.services.resourceStore.data[initialLanguage];
Matija Obreza's avatar
Matija Obreza committed
59
        if (initialLanguage !== defaultLanguage) {
60
          // So that we have missing translations
Matija Obreza's avatar
Matija Obreza committed
61
          initialI18nStore[defaultLanguage] = req.i18n.services.resourceStore.data[defaultLanguage];
62
        }
Matija Obreza's avatar
Matija Obreza committed
63
64
        const basename = initialLanguage !== defaultLanguage ? `${config.frontendPath}/${initialLanguage}` : `${config.frontendPath}`;
        const pathWithoutLang = req.url.substr(initialLanguage !== defaultLanguage ? 3 : 0, req.url.length);
65
66

        console.log(`<StaticRouter location="${pathWithoutLang}" basename="${basename}"`);
Maxym Borodenko's avatar
Maxym Borodenko committed
67
68
69
70
71
72

        const InitialView = (
            <ReduxProvider store={ store }>
                <JssProvider generateClassName={ generateClassName } registry={ sheets }>
                    <MuiThemeProvider theme={ theme } sheetsManager={ new Map() }>
                        <I18nextProvider i18n={ req.i18n }>
73
                            <StaticRouter location={ pathWithoutLang } context={ context } basename={ basename }>
Maxym Borodenko's avatar
Maxym Borodenko committed
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
                                { renderRoutes(routes) }
                            </StaticRouter>
                        </I18nextProvider>
                    </MuiThemeProvider>
                </JssProvider>
            </ReduxProvider>
        );

        const componentHTML = renderToString(InitialView);
        const initialState = store.getState();
        const titleState = store.getState().pageTitle.title;

        if (process.env.DEBUG) {
            console.log('serverCSS:', sheets.toString());
            console.log('componentHTML:', componentHTML);
            console.log('initialState:', initialState);
        }

92
93
94
95
96
97
98
99
100
101
102
103
104
        return html.replace(/SERVER_RENDERED_(CSS|STATE|HTML|TITLE|I18NSTORE|DIR|LANG)/g, (match, x) => {
            console.log(`Injecting ${match}`);
            switch (match) {
              case 'SERVER_RENDERED_CSS': return sheets.toString();
              case 'SERVER_RENDERED_STATE': return serialize(initialState, {isJSON: true});
              case 'SERVER_RENDERED_HTML': return componentHTML;
              case 'SERVER_RENDERED_TITLE': return titleState;
              case 'SERVER_RENDERED_I18NSTORE': return serialize(initialI18nStore, {isJSON: true});
              case 'SERVER_RENDERED_DIR': return serialize(getDir(initialLanguage), {isJSON: false});
              case 'SERVER_RENDERED_LANG': return serialize(initialLanguage, {isJSON: false});
              default: console.log(`Unrecognized variable in ssr-template.html: ${match}`); return '';
            }
        });
Maxym Borodenko's avatar
Maxym Borodenko committed
105
106
107
108
    }

    setLocale();

Maxym Borodenko's avatar
Maxym Borodenko committed
109
110
111
112
113
114
115
    checkAuthToken(req, res, store.dispatch).then(() => {
        const language = req.i18n.language;
        const pathWithoutLang = pathname.substr(language !== 'en' ? 3 : 0, pathname.length);
        console.log(`Rendering ${pathWithoutLang} for ${pathname}`);
        const branch = matchRoutes(routes, pathWithoutLang);

        fetchComponentData(store.dispatch, branch, search)
116
117
118
119
120
121
122
123
        .catch((err) => {
            console.log('Error fetching component data', err);
            res.status(500).end(err.message);
            // const errFilledHtml = makeErrorHtml(errHtml, err);
            // res.status(500).set('Content-Type', 'text/html').send(errFilledHtml);
        })
        .then(() => {
            return renderView();
Maxym Borodenko's avatar
Maxym Borodenko committed
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
        }).then((html) => {
            const serverRenderTime = `${Date.now() - startTime}ms`;
            console.log('Server render time:', startTime, Date.now(), serverRenderTime);

            const keyStatus = 'status';
            const keyUrl = 'url';
            const status = context[keyStatus];
            const url = context[keyUrl];

            if (status && status === 404) {
                res.status(404);
            }
            if (url) {
                if (url === '/login') {
                    res.status(401);
                } else {
                    res.status(302);
                }
Maxym Borodenko's avatar
Maxym Borodenko committed
142
143
            }

144
            return res.set('Content-Type', 'text/html').send(html);
Maxym Borodenko's avatar
Maxym Borodenko committed
145
146
147
148
        }).catch((err) => {
            console.error('Error:', err);
            res.status(500).end(err.message);
        });
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
149
150
151
152
    }).catch((err) => {
      console.error('Error:', err.message);
      const errFilledHtml = makeErrorHtml(errHtml, err);
      res.status(503).set('Content-Type', 'text/html').send(errFilledHtml);
153
    });
Maxym Borodenko's avatar
Maxym Borodenko committed
154
155
};

Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
156
const makeErrorHtml = (template, error) => {
157
  const theFilledHtml = template.replace('ERROR_MESSAGE', error.status).replace('ERROR_DETAILS', error.data);
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
158
159
160
161

  return theFilledHtml;
};

Maxym Borodenko's avatar
Maxym Borodenko committed
162
export default prerenderer;