@zengenti/contensis-react-base 3.3.2-beta.1 → 4.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/cjs/{App-B2ohFzUt.js → App-vZrUfVgQ.js} +7 -20
  2. package/cjs/{App-B2ohFzUt.js.map → App-vZrUfVgQ.js.map} +1 -1
  3. package/cjs/{ChangePassword.container-Dup9_na7.js → ChangePassword.container-ECjEXixF.js} +2 -2
  4. package/cjs/{ChangePassword.container-Dup9_na7.js.map → ChangePassword.container-ECjEXixF.js.map} +1 -1
  5. package/cjs/CookieHelper.class-C3Eqoze9.js +471 -0
  6. package/cjs/CookieHelper.class-C3Eqoze9.js.map +1 -0
  7. package/cjs/{RouteLoader-De-dhkg-.js → RouteLoader-D5Yg7EB5.js} +143 -44
  8. package/cjs/RouteLoader-D5Yg7EB5.js.map +1 -0
  9. package/cjs/{SSRContext-DpnwQ2te.js → SSRContext-DVj_QAC1.js} +23 -9
  10. package/cjs/SSRContext-DVj_QAC1.js.map +1 -0
  11. package/cjs/ToJs-C9jwV7YB.js.map +1 -1
  12. package/cjs/VersionInfo-B_dKCubg.js +204 -0
  13. package/cjs/VersionInfo-B_dKCubg.js.map +1 -0
  14. package/cjs/client.js +53 -20
  15. package/cjs/client.js.map +1 -1
  16. package/cjs/contensis-react-base.js +409 -134
  17. package/cjs/contensis-react-base.js.map +1 -1
  18. package/cjs/fromJSLeaveImmer-Blvlk4t2.js.map +1 -1
  19. package/cjs/redux.js +3 -3
  20. package/cjs/routing.js +8 -7
  21. package/cjs/routing.js.map +1 -1
  22. package/cjs/{sagas-Ekfrk7xA.js → sagas-BVX4Ps1e.js} +326 -130
  23. package/cjs/sagas-BVX4Ps1e.js.map +1 -0
  24. package/cjs/search.js +3 -194
  25. package/cjs/search.js.map +1 -1
  26. package/cjs/selectors-wCs5fHD4.js.map +1 -1
  27. package/cjs/{store-BihH67lI.js → store-D07FOXvM.js} +6 -9
  28. package/cjs/store-D07FOXvM.js.map +1 -0
  29. package/cjs/user.js +2 -2
  30. package/cjs/user.js.map +1 -1
  31. package/cjs/util.js +17 -182
  32. package/cjs/util.js.map +1 -1
  33. package/cjs/{version-Cg79mdPg.js → version-B7XFkBhY.js} +2 -2
  34. package/{esm/version-BnnERhzW.js.map → cjs/version-B7XFkBhY.js.map} +1 -1
  35. package/cjs/version-CM-bJ62L.js.map +1 -1
  36. package/esm/{App-BPsH6nHc.js → App-DLZweVSp.js} +10 -23
  37. package/esm/{App-BPsH6nHc.js.map → App-DLZweVSp.js.map} +1 -1
  38. package/esm/{ChangePassword.container-Bcpef423.js → ChangePassword.container-BgzIy8dA.js} +5 -5
  39. package/esm/{ChangePassword.container-Bcpef423.js.map → ChangePassword.container-BgzIy8dA.js.map} +1 -1
  40. package/esm/CookieHelper.class-FTURFpz3.js +464 -0
  41. package/esm/CookieHelper.class-FTURFpz3.js.map +1 -0
  42. package/esm/{RouteLoader-CipkGOgr.js → RouteLoader-xeQBXywk.js} +143 -49
  43. package/esm/RouteLoader-xeQBXywk.js.map +1 -0
  44. package/esm/{SSRContext-3TvaCDn0.js → SSRContext-BE8ElZ3X.js} +26 -12
  45. package/esm/SSRContext-BE8ElZ3X.js.map +1 -0
  46. package/esm/{ToJs-B4MH53fx.js → ToJs-CNzfvyxJ.js} +3 -3
  47. package/esm/{ToJs-B4MH53fx.js.map → ToJs-CNzfvyxJ.js.map} +1 -1
  48. package/esm/VersionInfo-Cno7K0OA.js +193 -0
  49. package/esm/VersionInfo-Cno7K0OA.js.map +1 -0
  50. package/esm/client.js +57 -25
  51. package/esm/client.js.map +1 -1
  52. package/esm/contensis-react-base.js +407 -132
  53. package/esm/contensis-react-base.js.map +1 -1
  54. package/esm/fromJSLeaveImmer-C_YACmOf.js.map +1 -1
  55. package/esm/redux.js +8 -8
  56. package/esm/routing.js +7 -9
  57. package/esm/routing.js.map +1 -1
  58. package/esm/{sagas-Cd05ZBBH.js → sagas-JI51CS37.js} +311 -115
  59. package/esm/sagas-JI51CS37.js.map +1 -0
  60. package/esm/search.js +21 -212
  61. package/esm/search.js.map +1 -1
  62. package/esm/{selectors-BRzliwbK.js → selectors-DO2ocdOp.js} +2 -2
  63. package/esm/selectors-DO2ocdOp.js.map +1 -0
  64. package/esm/{store-f0WxNWUu.js → store-3u0RzHZ0.js} +7 -9
  65. package/esm/store-3u0RzHZ0.js.map +1 -0
  66. package/esm/user.js +6 -6
  67. package/esm/user.js.map +1 -1
  68. package/esm/util.js +14 -177
  69. package/esm/util.js.map +1 -1
  70. package/esm/{version-BnnERhzW.js → version-BlsI7hX2.js} +3 -3
  71. package/{cjs/version-Cg79mdPg.js.map → esm/version-BlsI7hX2.js.map} +1 -1
  72. package/esm/{version-78jjDnHU.js → version-wnf-TITV.js} +2 -2
  73. package/esm/{version-78jjDnHU.js.map → version-wnf-TITV.js.map} +1 -1
  74. package/models/app/App.d.ts +2 -2
  75. package/models/app/pages/VersionInfo/components/VersionInfo.d.ts +7 -2
  76. package/models/models/AppState.d.ts +4 -5
  77. package/models/models/GetRouteActionArgs.d.ts +2 -2
  78. package/models/models/MatchedRoute.d.ts +4 -0
  79. package/models/models/SSRContext.d.ts +23 -0
  80. package/models/models/StaticRoute.d.ts +8 -4
  81. package/models/models/config/AppConfig.d.ts +1 -0
  82. package/models/models/config/ServerConfig.d.ts +1 -0
  83. package/models/models/config/StateType.d.ts +1 -0
  84. package/models/models/index.d.ts +1 -0
  85. package/models/redux/store/history.d.ts +2 -2
  86. package/models/redux/store/injectors.d.ts +4 -4
  87. package/models/redux/store/store.d.ts +1 -1
  88. package/models/routing/components/Redirect.d.ts +5 -0
  89. package/models/routing/components/RouteLoader.d.ts +2 -2
  90. package/models/routing/components/StaticRouteLoader.d.ts +6 -0
  91. package/models/routing/httpContext.d.ts +7 -0
  92. package/models/routing/index.d.ts +3 -0
  93. package/models/routing/redux/actions.d.ts +2 -3
  94. package/models/search/containers/withListing.d.ts +4 -1
  95. package/models/search/containers/withSearch.d.ts +4 -1
  96. package/models/server/features/response-handler/render-stream.d.ts +26 -0
  97. package/models/server/util/html.d.ts +23 -0
  98. package/models/server/util/jsx.d.ts +55 -0
  99. package/models/user/hocs/withRegistration.d.ts +6 -3
  100. package/models/util/ContensisDeliveryApi.d.ts +1 -2
  101. package/models/util/SSRContext.d.ts +17 -6
  102. package/models/util/mergeStaticRoutes.d.ts +1 -0
  103. package/package.json +25 -30
  104. package/cjs/CookieHelper.class-CxeVo9EP.js +0 -489
  105. package/cjs/CookieHelper.class-CxeVo9EP.js.map +0 -1
  106. package/cjs/RouteLoader-De-dhkg-.js.map +0 -1
  107. package/cjs/SSRContext-DpnwQ2te.js.map +0 -1
  108. package/cjs/forms.js +0 -5673
  109. package/cjs/forms.js.map +0 -1
  110. package/cjs/sagas-Ekfrk7xA.js.map +0 -1
  111. package/cjs/store-BihH67lI.js.map +0 -1
  112. package/cjs/urls-DVIwGZmd.js +0 -25
  113. package/cjs/urls-DVIwGZmd.js.map +0 -1
  114. package/esm/CookieHelper.class-W_NNNJKT.js +0 -482
  115. package/esm/CookieHelper.class-W_NNNJKT.js.map +0 -1
  116. package/esm/RouteLoader-CipkGOgr.js.map +0 -1
  117. package/esm/SSRContext-3TvaCDn0.js.map +0 -1
  118. package/esm/forms.js +0 -5661
  119. package/esm/forms.js.map +0 -1
  120. package/esm/sagas-Cd05ZBBH.js.map +0 -1
  121. package/esm/selectors-BRzliwbK.js.map +0 -1
  122. package/esm/store-f0WxNWUu.js.map +0 -1
  123. package/esm/urls-DfCisos-.js +0 -22
  124. package/esm/urls-DfCisos-.js.map +0 -1
  125. package/models/forms/index.d.ts +0 -1
@@ -1,16 +1,16 @@
1
- import { c as cachedSearch, d as deliveryApi, S as SSRContextProvider } from './SSRContext-3TvaCDn0.js';
1
+ import { c as cachedSearch, S as SSRContextProvider, d as deliveryApi } from './SSRContext-BE8ElZ3X.js';
2
2
  import { Query as Query$1 } from 'contensis-delivery-api';
3
3
  import React from 'react';
4
4
  import { Provider } from 'react-redux';
5
5
  import mapJson from 'jsonpath-mapper';
6
+ import { a8 as defaultExpressions, a9 as termExpressions, aa as contentTypeIdExpression, ab as filterExpressions, ac as orderByExpression, ad as customWhereExpressions, ae as cloneDeep } from './sagas-JI51CS37.js';
6
7
  import 'reselect';
7
- import 'deepmerge';
8
- import 'query-string';
9
- import { a8 as defaultExpressions, a9 as contentTypeIdExpression, aa as filterExpressions, ab as termExpressions, ac as orderByExpression, ad as customWhereExpressions, ae as cloneDeep } from './sagas-Cd05ZBBH.js';
10
8
  import 'immer';
11
9
  import 'deep-equal';
10
+ import 'deepmerge';
11
+ import 'query-string';
12
12
  import { Op, Query } from 'contensis-core-api';
13
- import { s as setCachingHeaders, u as url } from './urls-DfCisos-.js';
13
+ import { s as setCachingHeaders, u as url } from './VersionInfo-Cno7K0OA.js';
14
14
  import 'isomorphic-fetch';
15
15
  import express from 'express';
16
16
  import http from 'http';
@@ -18,39 +18,39 @@ import httpProxy from 'http-proxy';
18
18
  import fs from 'fs';
19
19
  import path from 'path';
20
20
  import { path as path$1 } from 'app-root-path';
21
- import { renderToString } from 'react-dom/server';
22
- import { StaticRouter } from 'react-router-dom';
23
- import { matchRoutes } from 'react-router-config';
21
+ import { renderToPipeableStream, renderToString } from 'react-dom/server';
22
+ import { matchRoutes } from 'react-router-dom';
24
23
  import { Helmet } from 'react-helmet';
25
24
  import { ServerStyleSheet } from 'styled-components';
26
25
  import serialize from 'serialize-javascript';
27
- import minifyCssString from 'minify-css-string';
28
- import { ChunkExtractor, ChunkExtractorManager } from '@loadable/server';
29
- import { identity, noop } from 'lodash';
26
+ import { noop, identity } from 'lodash';
30
27
  import { buildCleaner } from 'lodash-clean';
31
- import { CookiesProvider } from 'react-cookie';
32
- import { a as Cookies } from './CookieHelper.class-W_NNNJKT.js';
28
+ import { a as Cookies } from './CookieHelper.class-FTURFpz3.js';
33
29
  import cookiesMiddleware from 'universal-cookie-express';
34
- import { c as createStore } from './store-f0WxNWUu.js';
35
- import { h as history, p as pickProject, r as rootSaga } from './App-BPsH6nHc.js';
36
- export { A as ReactApp } from './App-BPsH6nHc.js';
37
- import { s as setVersionStatus, d as setVersion } from './version-BnnERhzW.js';
38
- import { a3 as selectSurrogateKeys, a4 as selectSsrApiCalls, e as selectRouteEntry, l as selectCurrentProject, g as getImmutableOrJS, s as setCurrentProject, K as selectCurrentSearch } from './selectors-BRzliwbK.js';
30
+ import { c as createStore } from './store-3u0RzHZ0.js';
31
+ import { h as history, p as pickProject, r as rootSaga } from './App-DLZweVSp.js';
32
+ export { A as ReactApp } from './App-DLZweVSp.js';
33
+ import { s as setVersionStatus, d as setVersion } from './version-BlsI7hX2.js';
34
+ import { a3 as selectSurrogateKeys, a4 as selectSsrApiCalls, h as selectRouteEntry, n as selectCurrentProject, g as getImmutableOrJS, d as setCurrentProject, K as selectCurrentSearch } from './selectors-DO2ocdOp.js';
35
+ import { H as HttpContext, m as mergeStaticRoutes } from './RouteLoader-xeQBXywk.js';
36
+ import { Transform } from 'stream';
37
+ import { ChunkExtractor, ChunkExtractorManager } from '@loadable/server';
39
38
  import chalk from 'chalk';
39
+ import minifyCssString from 'minify-css-string';
40
+ import { CookiesProvider } from 'react-cookie';
41
+ import { StaticRouter } from 'react-router-dom/server';
40
42
  import 'loglevel';
41
43
  import '@redux-saga/core/effects';
42
44
  import './_commonjsHelpers-BFTU3MAI.js';
45
+ import './version-wnf-TITV.js';
43
46
  import 'redux';
44
47
  import 'redux-thunk';
45
48
  import 'redux-saga';
46
- import 'redux-injectors';
49
+ import 'redux-injectors-19';
47
50
  import 'history';
48
51
  import 'await-to-js';
49
- import './version-78jjDnHU.js';
50
- import './ChangePassword.container-Bcpef423.js';
51
- import './ToJs-B4MH53fx.js';
52
- import 'react-hot-loader';
53
- import './RouteLoader-CipkGOgr.js';
52
+ import './ChangePassword.container-BgzIy8dA.js';
53
+ import './ToJs-CNzfvyxJ.js';
54
54
 
55
55
  /**
56
56
  * Util class holds our search results helper boilerplate methods
@@ -618,32 +618,29 @@ const assetProxy = httpProxy.createProxyServer();
618
618
  const deliveryProxy = httpProxy.createProxyServer();
619
619
  const reverseProxies = (app, reverseProxyPaths = []) => {
620
620
  deliveryApiProxy(deliveryProxy, app);
621
- app.all(reverseProxyPaths, (req, res) => {
621
+ app.all(reverseProxyPaths.map(proxyPath =>
622
+ // Patch to update paths for express v5
623
+ proxyPath.endsWith('/*') ? `${proxyPath.slice(0, -2)}/{*splat}` : proxyPath), (req, res) => {
622
624
  const target = req.hostname.indexOf('preview-') || req.hostname.indexOf('preview.') || req.hostname === 'localhost' ? servers$1.previewIis || servers$1.iis : servers$1.iis;
623
625
  assetProxy.web(req, res, {
624
626
  target,
625
627
  changeOrigin: true
626
628
  });
627
629
  assetProxy.on('error', e => {
628
- /* eslint-disable no-console */
629
630
  console.log(`Proxy Request for ${req.path} HostName:${req.hostname} failed with ${e}`);
630
- /* eslint-enable no-console */
631
631
  });
632
632
  });
633
633
  };
634
634
  const deliveryApiProxy = (apiProxy, app) => {
635
635
  // This is just here to stop cors requests on localhost. In Production this is mapped using varnish.
636
- app.all(['/api/delivery/*', '/api/forms/*', '/api/image/*', '/authenticate/*'], (req, res) => {
637
- /* eslint-disable no-console */
636
+ app.all(['/api/delivery/{*splat}', '/api/forms/{*splat}', '/api/image/{*splat}', '/authenticate/{*splat}'], (req, res) => {
638
637
  console.log(`Proxying api request to ${servers$1.alias}`);
639
638
  apiProxy.web(req, res, {
640
639
  target: deliveryApiHostname,
641
640
  changeOrigin: true
642
641
  });
643
642
  apiProxy.on('error', e => {
644
- /* eslint-disable no-console */
645
643
  console.log(`Proxy request for ${req.path} HostName:${req.hostname} failed with ${e}`);
646
- /* eslint-enable no-console */
647
644
  });
648
645
  });
649
646
  };
@@ -794,6 +791,94 @@ const handleResponse = (request, response, content, send = 'send') => {
794
791
  response[send](content);
795
792
  };
796
793
 
794
+ /**
795
+ * Render React JSX (and surrounding HTML document) via React's
796
+ * renderToPipeableStream method
797
+ * @param getContextHtml a function to produce the correct HTML template that surrounds the JSX "App" with all available document assets injected
798
+ * @param jsx the JSX to render via a streamed response
799
+ * @param response the express Response object
800
+ * @param stream all chunks are piped to this stream to add additional style elements to each streamed chunk
801
+ */
802
+ const renderStream = (getContextHtml, jsx, response, stream) => {
803
+ const {
804
+ abort,
805
+ pipe
806
+ } = renderToPipeableStream(jsx, {
807
+ onShellReady() {
808
+ const html = getContextHtml(false);
809
+ if (!html) {
810
+ // this means we have finished with the response already
811
+ abort();
812
+ } else {
813
+ const header = html.split('{{APP}}')[0];
814
+ response.setHeader('content-type', 'text/html; charset=utf-8');
815
+ stream.write(header);
816
+ pipe(stream);
817
+ }
818
+ },
819
+ onAllReady() {
820
+ const footer = getContextHtml(true).split('{{APP}}')[1];
821
+ stream.write(footer);
822
+ },
823
+ onShellError(error) {
824
+ response.statusCode = 500;
825
+ response.setHeader('content-type', 'text/html; charset=utf-8');
826
+ response.send('<h1>Something went wrong</h1>');
827
+ console.error(`[renderToPipeableStream:onShellError]`, error);
828
+ },
829
+ onError(error) {
830
+ console.error(`[renderToPipeableStream:onError]`, error);
831
+ }
832
+ });
833
+
834
+ // Abandon and switch to client rendering if enough time passes.
835
+ // Try lowering this to see the client recover.
836
+ setTimeout(() => abort(), 30 * 1000);
837
+ stream === null || stream === void 0 || stream.pipe(response);
838
+ };
839
+
840
+ /**
841
+ * Generate and add styled-components CSS to the streamed
842
+ * chunks of rendered HTML via renderToPipeableStream
843
+ *
844
+ * Workaround for Styled Components issue: React 18 Streaming SSR #3658
845
+ * https://github.com/styled-components/styled-components/issues/3658#issuecomment-2480721193
846
+ * credit: https://github.com/rurquia/styled-components-ssr-3658/blob/main/server/render.js
847
+ * @param sheet styled-components ServerStyleSheet
848
+ * @returns Transform Stream
849
+ */
850
+ const styledComponentsStream = sheet => {
851
+ const readerWriter = new Transform({
852
+ objectMode: true,
853
+ transform(chunk, /* encoding */
854
+ _, callback) {
855
+ // Get the chunk and retrieve the sheet's CSS as an HTML chunk,
856
+ // then reset its rules so we get only new ones for the next chunk
857
+ const renderedHtml = typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk);
858
+ const styledCSS = sheet._emitSheetCSS();
859
+ const CLOSING_TAG_R = /<\/[a-z]*>/i;
860
+ sheet.instance.clearTag();
861
+
862
+ // prepend style html to chunk, unless the start of the chunk is a
863
+ // closing tag in which case append right after that
864
+ if (/<\/head>/.test(renderedHtml)) {
865
+ const replacedHtml = renderedHtml.replace('</head>', `${styledCSS}</head>`);
866
+ this.push(replacedHtml);
867
+ } else if (CLOSING_TAG_R.test(renderedHtml)) {
868
+ const execResult = CLOSING_TAG_R.exec(renderedHtml);
869
+ const endOfClosingTag = execResult.index + execResult.flat().length - 1;
870
+ const before = renderedHtml.slice(0, endOfClosingTag);
871
+ const after = renderedHtml.slice(endOfClosingTag);
872
+ this.push(before + styledCSS + after);
873
+ } else {
874
+ this.push(styledCSS + renderedHtml);
875
+ }
876
+ callback();
877
+ }
878
+ });
879
+ return readerWriter;
880
+ };
881
+
797
882
  const readFileSync = path => fs.readFileSync(path, 'utf8');
798
883
  const loadableBundleData = ({
799
884
  stats,
@@ -841,7 +926,8 @@ const loadableChunkExtractors = () => {
841
926
  statsFile: path.resolve('dist/legacy/loadable-stats.json')
842
927
  });
843
928
  } catch (e) {
844
- console.info('@loadable/server legacy ChunkExtractor not available');
929
+ // legacy bundling deprecated in v4
930
+ // console.info('@loadable/server legacy ChunkExtractor not available');
845
931
  }
846
932
  commonLoadableExtractor.addChunk = chunk => {
847
933
  var _modern, _legacy, _legacy2;
@@ -919,6 +1005,36 @@ const getBundleTags = (loadableExtractor, scripts, staticRoutePath = 'static') =
919
1005
  return startupTag;
920
1006
  };
921
1007
 
1008
+ const getVersionInfo = staticFolderPath => {
1009
+ try {
1010
+ const versionData = fs.readFileSync(`dist/${staticFolderPath}/version.json`, 'utf8');
1011
+ const versionInfo = JSON.parse(versionData);
1012
+ return versionInfo;
1013
+ } catch (ex) {
1014
+ console.error(`Unable to read from "version.json"`, ex);
1015
+ return {};
1016
+ }
1017
+ };
1018
+
1019
+ /* eslint-disable no-console */
1020
+
1021
+ // Default exception types to add event listeners for
1022
+ const handleDefaultEvents = ['uncaughtException', 'unhandledRejection'];
1023
+ const unhandledExceptionHandler = (handleExceptions = handleDefaultEvents) => {
1024
+ const exceptionTypes = Array.isArray(handleExceptions) ? handleExceptions : handleExceptions === false ? [] : handleDefaultEvents;
1025
+ for (const type of exceptionTypes) {
1026
+ process.on(type, err => {
1027
+ if (err && err instanceof Error) {
1028
+ // Print a message to inform admins and developers the error should not be ignored
1029
+ console.log(`${`[contensis-react-base] ❌ ${chalk.red.bold(`${type} - ${err.message}`)}`}`);
1030
+ console.log(chalk.gray` - you are seeing this because we have tried to prevent the app from completely crashing - you should not ignore this problem`);
1031
+ // Log the error to server console
1032
+ console.error(err);
1033
+ }
1034
+ });
1035
+ }
1036
+ };
1037
+
922
1038
  const addStandardHeaders = (state, response, packagejson, groups) => {
923
1039
  if (state) {
924
1040
  try {
@@ -964,39 +1080,111 @@ const addVarnishAuthenticationHeaders = (state, response, groups = {}) => {
964
1080
  }
965
1081
  };
966
1082
 
967
- const getVersionInfo = staticFolderPath => {
968
- try {
969
- const versionData = fs.readFileSync(`dist/${staticFolderPath}/version.json`, 'utf8');
970
- const versionInfo = JSON.parse(versionData);
971
- return versionInfo;
972
- } catch (ex) {
973
- console.error(`Unable to read from "version.json"`, ex);
974
- return {};
1083
+ /**
1084
+ * Add assets to templateHTML in the positions represented
1085
+ * by replacing specific keys wrapped in handlebars depending
1086
+ * on the accessMethod(s) that have been set (or updated)
1087
+ * while processing the request
1088
+ */
1089
+ const replaceHtml = ({
1090
+ bundleTags = '',
1091
+ html = '',
1092
+ htmlAttributes = '',
1093
+ metadata = '',
1094
+ state = '',
1095
+ styleTags = '',
1096
+ title = '',
1097
+ templateHTML = '',
1098
+ templateHTMLFragment = '',
1099
+ templateHTMLStatic = ''
1100
+ }, accessMethod) => {
1101
+ let responseHTML = '';
1102
+ // Serve a blank HTML page with client scripts to load the app in the browser
1103
+ if (accessMethod.DYNAMIC) {
1104
+ responseHTML = templateHTML.replace('{{TITLE}}', '').replace('{{SEO_CRITICAL_METADATA}}', '').replace('{{CRITICAL_CSS}}', '').replace('{{APP}}', '')
1105
+ // .replace('{{LOADABLE_CHUNKS}}', bundleTags)
1106
+ .replace('{{REDUX_DATA}}', state);
975
1107
  }
976
- };
977
1108
 
978
- /* eslint-disable no-console */
1109
+ // Page fragment served with client scripts and redux data that hydrate the app client side
1110
+ else if (accessMethod.FRAGMENT && !accessMethod.STATIC) {
1111
+ responseHTML = templateHTMLFragment.replace('{{TITLE}}', title).replace('{{SEO_CRITICAL_METADATA}}', metadata).replace('{{CRITICAL_CSS}}', minifyCssString(styleTags))
1112
+ //.replace('{{APP}}', html)
1113
+ // .replace('{{LOADABLE_CHUNKS}}', bundleTags)
1114
+ .replace('{{REDUX_DATA}}', state);
1115
+ }
979
1116
 
980
- // Default exception types to add event listeners for
981
- const handleDefaultEvents = ['uncaughtException', 'unhandledRejection'];
982
- const unhandledExceptionHandler = (handleExceptions = handleDefaultEvents) => {
983
- const exceptionTypes = Array.isArray(handleExceptions) ? handleExceptions : handleExceptions === false ? [] : handleDefaultEvents;
984
- for (const type of exceptionTypes) {
985
- process.on(type, err => {
986
- if (err && err instanceof Error) {
987
- // Print a message to inform admins and developers the error should not be ignored
988
- console.log(`${`[contensis-react-base] ${chalk.red.bold(`${type} - ${err.message}`)}`}`);
989
- console.log(chalk.gray` - you are seeing this because we have tried to prevent the app from completely crashing - you should not ignore this problem`);
990
- // Log the error to server console
991
- console.error(err);
992
- }
993
- });
1117
+ // Full HTML page served statically
1118
+ else if (!accessMethod.FRAGMENT && accessMethod.STATIC) {
1119
+ responseHTML = templateHTMLStatic.replace('{{TITLE}}', title).replace('{{SEO_CRITICAL_METADATA}}', metadata).replace('{{CRITICAL_CSS}}', minifyCssString(styleTags))
1120
+ //.replace('{{APP}}', html)
1121
+ .replace('{{LOADABLE_CHUNKS}}', '');
1122
+ }
1123
+
1124
+ // Full HTML page served with client scripts and redux data that hydrate the app client side
1125
+ else if (!accessMethod.FRAGMENT && !accessMethod.STATIC) {
1126
+ responseHTML = templateHTML.replace('{{TITLE}}', title).replace('{{SEO_CRITICAL_METADATA}}', metadata).replace('{{CRITICAL_CSS}}', styleTags)
1127
+ //.replace('{{APP}}', html)
1128
+ // .replace('{{LOADABLE_CHUNKS}}', bundleTags)
1129
+ .replace('{{REDUX_DATA}}', state);
1130
+ }
1131
+
1132
+ // If react-helmet htmlAttributes are being used,
1133
+ // replace the html tag with those attributes sepcified
1134
+ // e.g. (lang, dir etc.)
1135
+ if (htmlAttributes) {
1136
+ responseHTML = responseHTML.replace(/<html?.+?>/, `<html ${htmlAttributes}>`);
994
1137
  }
1138
+ responseHTML = html ? responseHTML.replace('{{APP}}', html) : responseHTML;
1139
+
1140
+ // Only replace bundle tags at the very end when we have rendered and are
1141
+ // streaming out the HTML "footer"
1142
+ if (bundleTags) responseHTML = responseHTML.replace('{{LOADABLE_CHUNKS}}', bundleTags);
1143
+ return responseHTML;
1144
+ };
1145
+
1146
+ /**
1147
+ * Produce the JSX wrapped in the necessary Providers
1148
+ * to render the app in SSR
1149
+ * @param ReactApp the JSX to render
1150
+ * @param { providers, props, ssrAssets }
1151
+ * @returns the final JSX to render decorated with all Provider and App props
1152
+ */
1153
+ const ssrJsxProducer = (ReactApp, {
1154
+ providers,
1155
+ props,
1156
+ ssrAssets
1157
+ }) => {
1158
+ var _providers$styledComp;
1159
+ // Recast ChunkExtractorManager to avoid TS error `Property 'children' does not exist on type...`
1160
+ const ChunkExtractor = ChunkExtractorManager;
1161
+ const jsx = /*#__PURE__*/React.createElement(ChunkExtractor, {
1162
+ extractor: providers.loadable.extractor
1163
+ }, /*#__PURE__*/React.createElement(CookiesProvider, {
1164
+ cookies: providers.cookies
1165
+ }, /*#__PURE__*/React.createElement(Provider, {
1166
+ store: providers.redux
1167
+ }, /*#__PURE__*/React.createElement(HttpContext.Provider, {
1168
+ value: providers.httpContext
1169
+ }, /*#__PURE__*/React.createElement(StaticRouter, {
1170
+ location: providers.router.url
1171
+ }, /*#__PURE__*/React.createElement(SSRContextProvider, {
1172
+ accessMethod: providers.ssrContext.accessMethod,
1173
+ request: providers.ssrContext.request,
1174
+ response: providers.ssrContext.response,
1175
+ ssrAssets: ssrAssets
1176
+ }, /*#__PURE__*/React.createElement(ReactApp, {
1177
+ routes: props.routes,
1178
+ withEvents: props.withEvents
1179
+ })))))));
1180
+
1181
+ // Wrap the JSX in a StyleSheetManager if a ServerStyleSheet is provided
1182
+ return !((_providers$styledComp = providers.styledComponents) !== null && _providers$styledComp !== void 0 && _providers$styledComp.sheet) ? jsx : providers.styledComponents.sheet.collectStyles(jsx);
995
1183
  };
996
1184
 
997
1185
  const webApp = (app, ReactApp, config) => {
998
1186
  const {
999
- stateType = 'immutable',
1187
+ stateType = 'js',
1000
1188
  routes,
1001
1189
  withReducers,
1002
1190
  withSagas,
@@ -1013,18 +1201,26 @@ const webApp = (app, ReactApp, config) => {
1013
1201
  handleExceptions = true
1014
1202
  } = config;
1015
1203
  const staticRoutePath = config.staticRoutePath || staticFolderPath;
1204
+ let isRenderingJsxToString = config.renderToString || false;
1016
1205
  const bundleData = getBundleData(config, staticRoutePath);
1017
1206
  const attributes = stringifyAttributes(scripts.attributes);
1018
1207
  scripts.startup = scripts.startup || startupScriptFilename;
1019
- const responseHandler = typeof handleResponses === 'function' ? handleResponses : handleResponse;
1208
+ let responseHandler = handleResponse;
1209
+ if (typeof handleResponses === 'function') {
1210
+ responseHandler = handleResponses;
1211
+ isRenderingJsxToString = true;
1212
+ }
1020
1213
  if (handleExceptions !== false) unhandledExceptionHandler(handleExceptions); // Create `process.on` event handlers for unhandled exceptions (Node v15+)
1021
1214
 
1022
1215
  const versionInfo = getVersionInfo(staticFolderPath);
1023
- app.get('/*', cookiesMiddleware(), async (request, response) => {
1216
+ app.get('/{*splat}', cookiesMiddleware(), async (request, response) => {
1024
1217
  const url = encodeURI(request.url);
1025
- const matchedStaticRoute = () => matchRoutes(routes.StaticRoutes, request.path);
1026
- const isStaticRoute = () => matchedStaticRoute().length > 0;
1027
- const staticRoute = isStaticRoute() && matchedStaticRoute()[0];
1218
+ const matchedStaticRoute = matchRoutes(routes.StaticRoutes, request.path);
1219
+ const isStaticRoute = matchedStaticRoute && matchedStaticRoute.length > 0;
1220
+ if (isStaticRoute) {
1221
+ mergeStaticRoutes(matchedStaticRoute);
1222
+ }
1223
+ const staticRoute = isStaticRoute ? matchedStaticRoute.pop() || null : null;
1028
1224
 
1029
1225
  // Allow certain routes to avoid SSR
1030
1226
  const onlyDynamic = staticRoute && staticRoute.route.ssr === false;
@@ -1046,9 +1242,6 @@ const webApp = (app, ReactApp, config) => {
1046
1242
  static: value
1047
1243
  }) => normaliseQs(value) || onlySSR
1048
1244
  });
1049
- const context = {};
1050
- // Track the current statusCode via the response object
1051
- response.status(200);
1052
1245
 
1053
1246
  // Create a store (with a memory history) from our current url
1054
1247
  const store = await createStore(withReducers, {}, history({
@@ -1074,22 +1267,38 @@ const webApp = (app, ReactApp, config) => {
1074
1267
  request.universalCookies :
1075
1268
  // this is a stub cookie collection so cookie methods can be used in code
1076
1269
  new Cookies();
1077
- const jsx = /*#__PURE__*/React.createElement(ChunkExtractorManager, {
1078
- extractor: loadableExtractor.commonLoadableExtractor
1079
- }, /*#__PURE__*/React.createElement(CookiesProvider, {
1080
- cookies: ssrCookies
1081
- }, /*#__PURE__*/React.createElement(Provider, {
1082
- store: store
1083
- }, /*#__PURE__*/React.createElement(StaticRouter, {
1084
- context: context,
1085
- location: url
1086
- }, /*#__PURE__*/React.createElement(SSRContextProvider, {
1087
- request: request,
1088
- response: response
1089
- }, /*#__PURE__*/React.createElement(ReactApp, {
1090
- routes: routes,
1091
- withEvents: withEvents
1092
- }))))));
1270
+
1271
+ // Track the current statusCode via the response object
1272
+ response.status(200);
1273
+
1274
+ // Create the context we will pass to JSX HttpContext.Provider
1275
+ // and read back any context props set by the ReactApp
1276
+ const context = {};
1277
+
1278
+ // Amalgamate all props for the various Providers we wrap the ReactApp with
1279
+ const jsxProviderProps = {
1280
+ loadable: {
1281
+ extractor: loadableExtractor.commonLoadableExtractor
1282
+ },
1283
+ cookies: ssrCookies,
1284
+ redux: store,
1285
+ httpContext: context,
1286
+ router: {
1287
+ url
1288
+ },
1289
+ ssrContext: {
1290
+ accessMethod,
1291
+ request,
1292
+ response
1293
+ }
1294
+ };
1295
+ // These are the props we will pass to the ReactApp itself
1296
+ const jsxReactAppProps = {
1297
+ routes,
1298
+ withEvents
1299
+ };
1300
+
1301
+ // Get the configured HTML templates provided by the consumer
1093
1302
  const {
1094
1303
  templateHTML = '',
1095
1304
  templateHTMLFragment = '',
@@ -1099,13 +1308,28 @@ const webApp = (app, ReactApp, config) => {
1099
1308
  // Serve a blank HTML page with client scripts to load the app in the browser
1100
1309
  if (accessMethod.DYNAMIC) {
1101
1310
  // Dynamic doesn't need sagas
1311
+ // or styles, or any split component bundles
1312
+ // nor are we streaming responses
1313
+ const isDynamicHints = `<script ${attributes}>window.versionStatus = "${versionStatus}"; window.isDynamic = true;</script>`;
1314
+ const jsx = ssrJsxProducer(ReactApp, {
1315
+ providers: jsxProviderProps,
1316
+ props: jsxReactAppProps,
1317
+ ssrAssets: {
1318
+ serializedState: isDynamicHints
1319
+ }
1320
+ });
1102
1321
  renderToString(jsx);
1103
1322
 
1104
1323
  // Dynamic page render has only the necessary bundles to start up the app
1105
1324
  // and does not include any react-loadable code-split bundles
1106
1325
  const bundleTags = getBundleTags(loadableExtractor, scripts, staticRoutePath);
1107
- const isDynamicHints = `<script ${attributes}>window.versionStatus = "${versionStatus}"; window.isDynamic = true;</script>`;
1108
- const responseHtmlDynamic = templateHTML.replace('{{TITLE}}', '').replace('{{SEO_CRITICAL_METADATA}}', '').replace('{{CRITICAL_CSS}}', '').replace('{{APP}}', '').replace('{{LOADABLE_CHUNKS}}', bundleTags).replace('{{REDUX_DATA}}', isDynamicHints);
1326
+ const responseHtmlDynamic = replaceHtml({
1327
+ bundleTags,
1328
+ state: isDynamicHints,
1329
+ templateHTML,
1330
+ templateHTMLFragment
1331
+ }, accessMethod);
1332
+
1109
1333
  // Dynamic pages always return a 200 so we can run
1110
1334
  // the app and serve up all errors inside the client
1111
1335
  response.setHeader('Surrogate-Control', `max-age=${getCacheDuration(200)}`);
@@ -1116,22 +1340,7 @@ const webApp = (app, ReactApp, config) => {
1116
1340
  if (!accessMethod.DYNAMIC) {
1117
1341
  store.runSaga(rootSaga(withSagas)).toPromise().then(() => {
1118
1342
  var _selectCurrentSearch;
1119
- const sheet = new ServerStyleSheet();
1120
- const html = renderToString(sheet.collectStyles(jsx));
1121
- const helmet = Helmet.renderStatic();
1122
- Helmet.rewind();
1123
- const htmlAttributes = helmet.htmlAttributes.toString();
1124
- let title = helmet.title.toString();
1125
- const metadata = helmet.meta.toString().concat(helmet.base.toString()).concat(helmet.link.toString()).concat(helmet.script.toString()).concat(helmet.noscript.toString());
1126
- if (context.url) {
1127
- return response.redirect(context.statusCode || 302, context.url);
1128
- }
1129
1343
  const reduxState = store.getState();
1130
- const styleTags = sheet.getStyleTags();
1131
-
1132
- // After running rootSaga there should be an additional react-loadable
1133
- // code-split bundles for any page components as well as core app bundles
1134
- const bundleTags = getBundleTags(loadableExtractor, scripts, staticRoutePath);
1135
1344
  let clonedState = buildCleaner({
1136
1345
  isArray: identity,
1137
1346
  isBoolean: identity,
@@ -1172,48 +1381,108 @@ const webApp = (app, ReactApp, config) => {
1172
1381
  serialisedReduxData = `<script ${attributes}>window.versionStatus = "${versionStatus}"; window.REDUX_DATA = ${serialisedReduxData}</script>`;
1173
1382
  }
1174
1383
  }
1175
- if ((context.statusCode || 200) >= 404) {
1176
- accessMethod.STATIC = true;
1177
- }
1178
1384
 
1179
1385
  // Responses
1180
- let responseHTML = '';
1181
- if (context.statusCode === 404) title = '<title>404 page not found</title>';
1386
+ addStandardHeaders(reduxState, response, packagejson, {
1387
+ allowedGroups,
1388
+ globalGroups
1389
+ });
1182
1390
 
1183
- // Static page served as a fragment
1184
- if (accessMethod.FRAGMENT && accessMethod.STATIC) {
1185
- responseHTML = minifyCssString(styleTags) + html;
1186
- }
1391
+ // // Produce the ssr jsx one time so we can get any style tags to pass back in
1392
+ // ssrJsxProducer(ReactApp, {
1393
+ // providers: { ...jsxProviderProps, styledComponents: { sheet } },
1394
+ // props: jsxReactAppProps,
1395
+ // });
1187
1396
 
1188
- // Page fragment served with client scripts and redux data that hydrate the app client side
1189
- if (accessMethod.FRAGMENT && !accessMethod.STATIC) {
1190
- responseHTML = templateHTMLFragment.replace('{{TITLE}}', title).replace('{{SEO_CRITICAL_METADATA}}', metadata).replace('{{CRITICAL_CSS}}', minifyCssString(styleTags)).replace('{{APP}}', html).replace('{{LOADABLE_CHUNKS}}', bundleTags).replace('{{REDUX_DATA}}', serialisedReduxData);
1191
- }
1397
+ // // After running rootSaga (and rendering subsquent children)
1398
+ // // there should be additional react-loadable
1399
+ // // code-split bundles for any page components as well as core app bundles
1400
+ // const bundleTags = getBundleTags(
1401
+ // loadableExtractor,
1402
+ // scripts,
1403
+ // staticRoutePath
1404
+ // );
1192
1405
 
1193
- // Full HTML page served statically
1194
- if (!accessMethod.FRAGMENT && accessMethod.STATIC) {
1195
- responseHTML = templateHTMLStatic.replace('{{TITLE}}', title).replace('{{SEO_CRITICAL_METADATA}}', metadata).replace('{{CRITICAL_CSS}}', minifyCssString(styleTags)).replace('{{APP}}', html).replace('{{LOADABLE_CHUNKS}}', '');
1196
- }
1406
+ const sheet = new ServerStyleSheet();
1407
+ const styledJsx = ssrJsxProducer(ReactApp, {
1408
+ providers: {
1409
+ ...jsxProviderProps,
1410
+ styledComponents: {
1411
+ sheet
1412
+ }
1413
+ },
1414
+ props: jsxReactAppProps,
1415
+ ssrAssets: {
1416
+ // bundleTags,
1417
+ // htmlAttributes,
1418
+ // metadata,
1419
+ // title,
1420
+ }
1421
+ });
1197
1422
 
1198
- // Full HTML page served with client scripts and redux data that hydrate the app client side
1199
- if (!accessMethod.FRAGMENT && !accessMethod.STATIC) {
1200
- responseHTML = templateHTML.replace('{{TITLE}}', title).replace('{{SEO_CRITICAL_METADATA}}', metadata).replace('{{CRITICAL_CSS}}', styleTags).replace('{{APP}}', html).replace('{{LOADABLE_CHUNKS}}', bundleTags).replace('{{REDUX_DATA}}', serialisedReduxData);
1201
- }
1423
+ // We have to call renderToString() in order for all components to have
1424
+ // had chance to set the helmet metadata
1425
+ const html = renderToString(styledJsx);
1426
+ // Helmet.renderStatic() has to be called synchronously immediately after calling renderToString()
1427
+ // as it is not thread-safe (or specifically scoped to only this request)
1428
+ const helmet = Helmet.renderStatic();
1202
1429
 
1203
- // Set response.status from React StaticRouter
1204
- if (typeof context.statusCode === 'number') response.status(context.statusCode);
1205
- addStandardHeaders(reduxState, response, packagejson, {
1206
- allowedGroups,
1207
- globalGroups
1208
- });
1430
+ // Because we have had to call renderToString() here to reliably gather all helmet metadata
1431
+ // We could potentially call sheet.getStyleTags() here too and avoid piping a react-rendered
1432
+ // stream to a second stream to inject styled-components CSS
1433
+
1434
+ const htmlAttributes = helmet.htmlAttributes.toString();
1435
+ let title = helmet.title.toString();
1436
+ const metadata = helmet.meta.toString().concat(helmet.base.toString()).concat(helmet.link.toString()).concat(helmet.script.toString()).concat(helmet.noscript.toString());
1209
1437
  try {
1210
- // If react-helmet htmlAttributes are being used,
1211
- // replace the html tag with those attributes sepcified
1212
- // e.g. (lang, dir etc.)
1213
- if (htmlAttributes) {
1214
- responseHTML = responseHTML.replace(/<html?.+?>/, `<html ${htmlAttributes}>`);
1438
+ /**
1439
+ * Loads all page assets into the provided templateHTML
1440
+ *
1441
+ * Is callable after the JSX has been rendered, as
1442
+ * JSX components may update the context via the
1443
+ * HttpContext.Provider which can influence whether
1444
+ * we render the page as STATIC or render nothing
1445
+ * if the context has requested a redirect
1446
+ * */
1447
+ const getContextHtml = (isFinal = false, styleTags, renderedJsxMarkup) => {
1448
+ if (context.url) {
1449
+ response.redirect(context.statusCode || 302, context.url);
1450
+ return '';
1451
+ }
1452
+
1453
+ // Make the page render statically if there is an error status code
1454
+ if ((context.statusCode || 200) >= 404) {
1455
+ accessMethod.STATIC = true;
1456
+ }
1457
+ if (context.statusCode === 404) title = '<title>404 page not found</title>';
1458
+
1459
+ // Set response.status from React StaticRouter
1460
+ if (typeof context.statusCode === 'number') response.status(context.statusCode);
1461
+ const bundleTags = isFinal ? getBundleTags(loadableExtractor, scripts, staticRoutePath) : '';
1462
+ const html = replaceHtml({
1463
+ bundleTags,
1464
+ html: renderedJsxMarkup,
1465
+ htmlAttributes,
1466
+ metadata,
1467
+ state: serialisedReduxData,
1468
+ styleTags,
1469
+ title,
1470
+ templateHTML,
1471
+ templateHTMLFragment,
1472
+ templateHTMLStatic
1473
+ }, accessMethod);
1474
+ return html;
1475
+ };
1476
+ if (isRenderingJsxToString) {
1477
+ // We have already (begrudgingly) rendered the JSX to a string above
1478
+ // so we can get all of the Helmet metadata out from any rendered component
1479
+ // const html = renderToString(styledJsx);
1480
+ const styleTags = sheet.getStyleTags();
1481
+ const responseHTML = getContextHtml(true, styleTags, html);
1482
+ responseHandler(request, response, responseHTML);
1483
+ } else {
1484
+ renderStream(getContextHtml, styledJsx, response, styledComponentsStream(sheet));
1215
1485
  }
1216
- responseHandler(request, response, responseHTML);
1217
1486
  } catch (err) {
1218
1487
  console.info(err.message);
1219
1488
  }
@@ -1224,7 +1493,13 @@ const webApp = (app, ReactApp, config) => {
1224
1493
  response.status(500);
1225
1494
  responseHandler(request, response, `Error occurred: <br />${err.stack} <br />${JSON.stringify(err)}`);
1226
1495
  });
1227
- renderToString(jsx);
1496
+
1497
+ // If this is removed we don't get the redux state populated
1498
+ // with the result of the actions RouteLoader component has dispatched
1499
+ renderToString(ssrJsxProducer(ReactApp, {
1500
+ providers: jsxProviderProps,
1501
+ props: jsxReactAppProps
1502
+ }));
1228
1503
  store.close();
1229
1504
  }
1230
1505
  });