prerenderer.tsx 9.86 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import * as React from 'react';
import { renderToString } from 'react-dom/server';

import Loadable from 'react-loadable';
import { getBundles } from 'react-loadable/webpack';

import { createMemoryHistory } from 'history';
import { StaticRouter } from 'react-router-dom';
import { Provider as ReduxProvider } from 'react-redux';
import * as serialize from 'serialize-javascript';
import { routerMiddleware } from 'connected-react-router';
import { createStore, applyMiddleware } from 'redux';
import { I18nextProvider } from 'react-i18next';
import thunk from 'redux-thunk';
import createSagaMiddleware from 'redux-saga'
import rootReducer from 'core/reducer';
import languages from '@gringlobal/i18n/data/Languages';


20
21
22
23
import { SheetsRegistry } from 'jss';
import { MuiThemeProvider, StylesProvider, createGenerateClassName } from '@material-ui/core/styles';
import theme from 'core/ui/theme';

24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// import checkAuthToken from './checkAuthToken';
import { routes } from 'core/ui/routes';
import matchRoutes from '@gringlobal/client/ui/matchRoutes';
import renderRoutes from '@gringlobal/client/ui/renderRoutes';
import fetchComponentData from 'server/middleware/fetchComponentData';
import detectLocaleFromPath from 'server/middleware/detectLocaleFromPath';
import getDir from 'server/middleware/detectDirection';
import config from 'server/config';
import checkAuthToken from 'server/middleware/checkAuthToken';
import { configure, receiveLang } from '@gringlobal/client/action/applicationConfig';
import ApiError from '@gringlobal/client/model/common/ApiError';

import sagas from 'core/action/saga';

// react-loadable webpack stats
import * as path from 'path';
import { readFileSync } from 'fs';
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
41
import { receiveSsrError } from 'core/action/serverInfo';
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
const reactLoadableStats = JSON.parse(readFileSync(path.join('./', 'react-loadable.json'), { encoding: 'utf8' }));
// console.log(`react-loadable stats: `, reactLoadableStats);


const prerenderer = (html, errHtml) => (req, res) => {
  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 his = createMemoryHistory();
  const sagaMiddleware = createSagaMiddleware();

  const store = createStore(rootReducer(), {} as any, applyMiddleware(thunk, sagaMiddleware, routerMiddleware(his)));
  // Only send public data to client
  store.dispatch(configure({ frontendPath: config.frontendPath, apiUrl: config.apiUrl, googleClientId: config.googleClientId, clientId: config.clientId, anonToken: config.access_token }));
  const sagaReady = sagaMiddleware.run(sagas);

59
  // console.log('Processing request', req._parsedOriginalUrl);
60
61
62
63
64
65
  const pathname = req._parsedOriginalUrl.pathname;
  const search = req._parsedOriginalUrl.search;
  const context = {};

  function setLocale() {
    const locale = detectLocaleFromPath(config.frontendPath, pathname, 0);
66
    // console.log('Detected locale for SSR is', locale);
67
68
69
    req.i18n.changeLanguage(locale);
  }

Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
70
  function renderView(error?) {
71
    // const jss = createJss(jssPreset());
72
73
74
    const sheets = new SheetsRegistry();
    const generateClassName = createGenerateClassName();

75
76
77
78
79
80
81
82
83
84
85
    const initialLanguage = req.i18n.language;
    const initialI18nStore = {};
    const defaultLanguage = 'en';
    initialI18nStore[initialLanguage] = req.i18n.services.resourceStore.data[initialLanguage];
    if (initialLanguage !== defaultLanguage) {
      // So that we have missing translations
      initialI18nStore[defaultLanguage] = req.i18n.services.resourceStore.data[defaultLanguage];
    }
    const basename = initialLanguage !== defaultLanguage ? `${config.frontendPath}/${initialLanguage}` : `${config.frontendPath}`;
    const pathWithoutLang = req.url.substr(initialLanguage !== defaultLanguage ? 3 : 0, req.url.length);
    console.log(`<StaticRouter location="${pathWithoutLang}" basename="${basename}"`);
86
    const direction = getDir(initialLanguage);
87
88
89
90
91
    const modules = [];

    const InitialView = (
      <Loadable.Capture report={ (moduleName) => modules.push(moduleName) }>
        <ReduxProvider store={ store }>
92
93
94
95
96
97
98
99
100
          <StylesProvider generateClassName={ generateClassName } sheetsRegistry={ sheets }>
            <MuiThemeProvider theme={ theme(direction) }>
              <I18nextProvider i18n={ req.i18n }>
                <StaticRouter location={ pathWithoutLang } context={ context } basename={ basename }>
                  { renderRoutes(routes) }
                </StaticRouter>
              </I18nextProvider>
            </MuiThemeProvider>
          </StylesProvider>
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
        </ReduxProvider>
      </Loadable.Capture>
    );

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

    // console.log('react-loadable modules', modules);
    const bundles = getBundles(reactLoadableStats, modules);
    // console.log(`react-loadable bundles`, bundles);
    const bundleStyles = bundles.filter((bundle) => bundle.file.endsWith('.css'));
    // console.log(`react-loadable bundleStyles`, bundleStyles);
    const bundleScripts = bundles.filter((bundle) => bundle.file.endsWith('.js'));
    // console.log(`react-loadable bundleScripts`, bundleScripts);

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

    const languageLinks = languages.map((lang) => {
      if (lang.short === 'en') {
        return `<link rel="alternate" hreflang="${lang.short}" href="${req.protocol + '://' + req.get('host')}${req.url}" />`;
      } else {
        return `<link rel="alternate" hreflang="${lang.short}" href="${req.protocol + '://' + req.get('host')}/${lang.short}${req.url}" />`;
      }
    });

    // HTML meta for activity posts

    return html.replace(/SERVER_RENDERED_(CSS|STATE|HTML|TITLE|I18NSTORE|DIR|LANG|HEADLINKS|BUNDLECSS|BUNDLESCRIPTS)/g, (match, x) => {
133
      // console.log(`Injecting ${match}`);
134
135
136
137
138
139
140
141
142
143
      switch (match) {
        case 'SERVER_RENDERED_BUNDLECSS': return bundleStyles.map((style) => {
          return `<link href="${style.file}" rel="stylesheet"/>`;
        }).join('\n');
        case 'SERVER_RENDERED_BUNDLESCRIPTS': return bundleScripts.map((bundle) => {
          return `<script src="${bundle.file}"></script>`;
          // alternatively if you are using publicPath option in webpack config
          // you can use the publicPath value from bundle, e.g:
          // return `<script src="${bundle.publicPath}"></script>`
        }).join('\n');
144
        case 'SERVER_RENDERED_CSS': return sheets.toString();
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
        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 });
        case 'SERVER_RENDERED_HEADLINKS': return languageLinks.join('');
        default: console.log(`Unrecognized variable in ssr-template.html: ${match}`); return '';
      }
    });
  }

  setLocale();

  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);

Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
165
166
167
168
169
170
    if (branch && branch.length > 0) {
      if (branch[branch.length - 1].route.status) {
        res.status(branch[branch.length - 1].route.status);
      }
    }

171
172
173
174
175
    store.dispatch(receiveLang(language));
    fetchComponentData(store.dispatch, branch, search, store.getState())
      .then(() => {
        return sagaReady.toPromise().then(() => {
          console.log('Fetched all component data');
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
          const html = renderView();
          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);
            }
          }

          return res.set('Content-Type', 'text/html').send(html);
197
        });
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
198
199
200
201
202
203
204
205
206
207
      })
      .catch((err) => {
        if (+err.status === 404) {
          console.log('Error while fetching component data, rendering 404 page for: ', pathWithoutLang);
          store.dispatch(receiveSsrError(404, pathWithoutLang));
          res.status(404);
          return renderView();
        }

        console.log('Error fetching component data', ApiError.axiosError(err));
208
209
210
        return res.status(500).end(makeErrorHtml(errHtml, {status: err.code || 500, data: err.message}, true));
        // res.status(500).set('Content-Type', 'text/html').send(errFilledHtml);
      })
211
212
213
214

  }).catch((err) => {
    console.error('Error:', err.message);
    const errFilledHtml = makeErrorHtml(errHtml, err, true);
215
    return res.status(503).set('Content-Type', 'text/html').send(errFilledHtml);
216
217
218
219
  });
};

const makeErrorHtml = (template, error, withExplanation = false) => {
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
220
  let theFilledHtml = template.replace('ERROR_MESSAGE', error.status).replace('ERROR_DETAILS', JSON.stringify(error.data));
221
222
223
224
225
226
227
  const explanation = 'The server is currently unavailable (because it is overloaded or down for maintenance). Generally, this is a temporary state.';
  theFilledHtml = theFilledHtml.replace('ERROR_EXPLANATION', withExplanation ? explanation : '');

  return theFilledHtml;
};

export default prerenderer;