Commit 85bc6230 authored by Matija Obreza's avatar Matija Obreza
Browse files

SSR with code splitting

parent f67cbe5b
......@@ -14,11 +14,6 @@
"plugins": [
"transform-object-rest-spread",
"babel-plugin-syntax-dynamic-import",
[
"import-inspector",
{
"serverSideRequirePath": true
}
]
"./react-loadable-custom/babel.js"
]
}
......@@ -2,4 +2,5 @@
.awesome-typescript-loader-cache/
node_modules
.idea
*.iml
\ No newline at end of file
*.iml
react-loadable.json
......@@ -3,6 +3,8 @@ const webpack = require('webpack');
const webpackMerge = require('webpack-merge');
const MinifyPlugin = require('terser-webpack-plugin');
const commonConfig = require('./webpack-base.config.js');
const CopyWebpackPlugin = require('copy-webpack-plugin');
// other
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
......@@ -55,6 +57,10 @@ module.exports = {
}
}),
new CopyWebpackPlugin([
{ from: 'react-loadable.json', to: 'react-loadable.json'},
]),
],
optimization: {
namedModules: true,
......
......@@ -9,6 +9,7 @@ const ManifestPlugin = require('webpack-manifest-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const GitRevisionPlugin = require('git-revision-webpack-plugin');
var gitRevisionPlugin = new GitRevisionPlugin();
const ReactLoadable = require('react-loadable/webpack');
// devserver configuration
const HOST = process.env.HOST || 'localhost';
......@@ -287,7 +288,11 @@ module.exports = {
{ from: 'locales', to: 'locales'},
{ from: 'node_modules/leaflet/dist/images', to: 'images'},
{ from: 'node_modules/ckeditor/', to: 'scripts/ckeditor'}
])
]),
new ReactLoadable.ReactLoadablePlugin({
filename: 'react-loadable.json',
}),
],
optimization: {
......
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import Loadable from 'react-loadable';
import {routerMiddleware, ConnectedRouter} from 'react-router-redux';
import createHistory from 'history/createBrowserHistory';
import {Provider} from 'react-redux';
......@@ -65,22 +67,24 @@ if (__PRELOADED_STATE__ === undefined) {
});
} else {
// SSR
ReactDOM.hydrate(
<Provider store={ store }>
<ConnectedRouter history={ history }>
<I18nextProvider i18n={ i18nClient } initialLanguage={ initialLanguage } initialI18nStore={ initialI18nStore }>
<MuiThemeProvider theme={ theme }>
{ renderRoutes(routes) }
</MuiThemeProvider>
</I18nextProvider>
</ConnectedRouter>
</Provider>,
document.getElementById('the-app'),
() => {
// We don't need the static css any more once we have launched our application.
log('Removing SSR-rendered styles');
const ssStyles = document.getElementById('server-side-styles');
ssStyles.parentNode.removeChild(ssStyles);
},
);
Loadable.preloadReady().then(() => {
ReactDOM.hydrate(
<Provider store={ store }>
<ConnectedRouter history={ history }>
<I18nextProvider i18n={ i18nClient } initialLanguage={ initialLanguage } initialI18nStore={ initialI18nStore }>
<MuiThemeProvider theme={ theme }>
{ renderRoutes(routes) }
</MuiThemeProvider>
</I18nextProvider>
</ConnectedRouter>
</Provider>,
document.getElementById('the-app'),
() => {
// We don't need the static css any more once we have launched our application.
log('Removing SSR-rendered styles');
const ssStyles = document.getElementById('server-side-styles');
ssStyles.parentNode.removeChild(ssStyles);
},
);
});
}
......@@ -453,6 +453,12 @@
}
}
},
"@types/webpack-env": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.13.6.tgz",
"integrity": "sha512-5Th3OsZ4gTRdr9Mho83BQ23cex4sRhOR4XTG+m+cJc0FhtUBK9Vn62hBJ+pnQYnSxoPOsKoAPOx6FcphxBC8ng==",
"dev": true
},
"@webassemblyjs/ast": {
"version": "1.7.11",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.11.tgz",
......@@ -1419,12 +1425,6 @@
"babel-runtime": "^6.22.0"
}
},
"babel-plugin-import-inspector": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-import-inspector/-/babel-plugin-import-inspector-2.0.0.tgz",
"integrity": "sha1-bUIjCfTxkjEzEM/eFVb3h1yuXQc=",
"dev": true
},
"babel-plugin-react-transform": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-react-transform/-/babel-plugin-react-transform-3.0.0.tgz",
......
'use strict';
// Copy of node_modules/react-loadable/lib/babel.js, MIT licensed.
// COPYRIGHT (c) 2017-present James Kyle <me@thejameskyle.com>
//
// Added our source 'utilities/LoadableUtil'
exports.__esModule = true;
exports.default = function (_ref) {
var t = _ref.types,
template = _ref.template;
return {
visitor: {
ImportDeclaration: function ImportDeclaration(path) {
var source = path.node.source.value;
// added our source
if (source !== 'utilities/LoadableUtil') return;
var defaultSpecifier = path.get('specifiers').find(function (specifier) {
return specifier.isImportDefaultSpecifier();
});
if (!defaultSpecifier) return;
var bindingName = defaultSpecifier.node.local.name;
var binding = path.scope.getBinding(bindingName);
binding.referencePaths.forEach(function (refPath) {
var callExpression = refPath.parentPath;
if (callExpression.isMemberExpression() && callExpression.node.computed === false && callExpression.get('property').isIdentifier({ name: 'Map' })) {
callExpression = callExpression.parentPath;
}
if (!callExpression.isCallExpression()) return;
var args = callExpression.get('arguments');
if (args.length !== 1) throw callExpression.error;
var options = args[0];
if (!options.isObjectExpression()) return;
var properties = options.get('properties');
var propertiesMap = {};
properties.forEach(function (property) {
var key = property.get('key');
propertiesMap[key.node.name] = property;
});
if (propertiesMap.webpack) {
// console.log('Babeling has webpack prop', propertiesMap);
return;
}
var loaderMethod = propertiesMap.loader.get('value');
var dynamicImports = [];
loaderMethod.traverse({
Import: function Import(path) {
dynamicImports.push(path.parentPath);
}
});
// console.log('Babeling dynamicImports', dynamicImports);
if (!dynamicImports.length) return;
propertiesMap.loader.insertAfter(t.objectProperty(t.identifier('webpack'), t.arrowFunctionExpression([], t.arrayExpression(dynamicImports.map(function (dynamicImport) {
return t.callExpression(t.memberExpression(t.identifier('require'), t.identifier('resolveWeak')), [dynamicImport.get('arguments')[0].node]);
})))));
propertiesMap.loader.insertAfter(t.objectProperty(t.identifier('modules'), t.arrayExpression(dynamicImports.map(function (dynamicImport) {
return dynamicImport.get('arguments')[0].node;
}))));
});
}
}
};
};
\ No newline at end of file
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/createMemoryHistory';
import { StaticRouter } from 'react-router-dom';
import { Provider as ReduxProvider } from 'react-redux';
......@@ -30,6 +34,13 @@ import checkAuthToken from './checkAuthToken';
import { configure, receiveLang } from 'actions/applicationConfig';
import ApiError from 'model/ApiError';
// react-loadable webpack stats
import * as path from 'path';
import {readFileSync} from 'fs';
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();
......@@ -67,24 +78,36 @@ const prerenderer = (html, errHtml) => (req, res) => {
console.log(`<StaticRouter location="${pathWithoutLang}" basename="${basename}"`);
const modules = [];
const InitialView = (
<ReduxProvider store={ store }>
<JssProvider generateClassName={ generateClassName } registry={ sheets }>
<MuiThemeProvider theme={ theme } sheetsManager={ new Map() }>
<I18nextProvider i18n={ req.i18n }>
<StaticRouter location={ pathWithoutLang } context={ context } basename={ basename }>
{ renderRoutes(routes) }
</StaticRouter>
</I18nextProvider>
</MuiThemeProvider>
</JssProvider>
</ReduxProvider>
<Loadable.Capture report={ (moduleName) => modules.push(moduleName) }>
<ReduxProvider store={ store }>
<JssProvider generateClassName={ generateClassName } registry={ sheets }>
<MuiThemeProvider theme={ theme } sheetsManager={ new Map() }>
<I18nextProvider i18n={ req.i18n }>
<StaticRouter location={ pathWithoutLang } context={ context } basename={ basename }>
{ renderRoutes(routes) }
</StaticRouter>
</I18nextProvider>
</MuiThemeProvider>
</JssProvider>
</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('serverCSS:', sheets.toString());
console.log('componentHTML:', componentHTML);
......@@ -99,9 +122,18 @@ const prerenderer = (html, errHtml) => (req, res) => {
}
});
return html.replace(/SERVER_RENDERED_(CSS|STATE|HTML|TITLE|I18NSTORE|DIR|LANG|HEADLINKS)/g, (match, x) => {
return html.replace(/SERVER_RENDERED_(CSS|STATE|HTML|TITLE|I18NSTORE|DIR|LANG|HEADLINKS|BUNDLECSS|BUNDLESCRIPTS)/g, (match, x) => {
console.log(`Injecting ${match}`);
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');
case 'SERVER_RENDERED_CSS': return sheets.toString();
case 'SERVER_RENDERED_STATE': return serialize(initialState, {isJSON: true});
case 'SERVER_RENDERED_HTML': return componentHTML;
......
......@@ -11,6 +11,7 @@
<style type="text/css" id="server-side-styles">
SERVER_RENDERED_CSS
</style>
SERVER_RENDERED_BUNDLECSS
</head>
<body>
......@@ -26,6 +27,7 @@
window.softwareVersion = <%= JSON.stringify(htmlWebpackPlugin.options.version) %>;
window.softwareCommit = <%= JSON.stringify(htmlWebpackPlugin.options.commithash) %>;
</script>
SERVER_RENDERED_BUNDLESCRIPTS
</body>
</html>
import { LoadableWrapper } from 'utilities/LoadableUtil';
import Loadable from 'utilities/LoadableUtil';
const publicRoutes = [
{
path: '/a/:filterCode(v.+)?',
component: new LoadableWrapper(() => import(/* webpackMode:"lazy", webpackChunkName: "accessions" */'accessions/ui/BrowsePage')),
component: Loadable({ loader: () => import(/* webpackMode:"lazy", webpackChunkName: "accessions" */'accessions/ui/BrowsePage') }),
exact: true,
},
{
path: '/a/overview/:filterCode(v.+)?',
component: new LoadableWrapper(() => import(/* webpackMode:"lazy", webpackChunkName: "accessions" */'accessions/ui/OverviewPage')),
component: Loadable({ loader: () => import(/* webpackMode:"lazy", webpackChunkName: "accessions" */'accessions/ui/OverviewPage') }),
exact: true,
},
{
path: '/a/map/:filterCode(v.+)?',
component: new LoadableWrapper(() => import(/* webpackMode:"lazy", webpackChunkName: "accessions" */'accessions/ui/MapPage')),
component: Loadable({ loader: () => import(/* webpackMode:"lazy", webpackChunkName: "accessions" */'accessions/ui/MapPage') }),
exact: true,
},
{
path: '/a/:uuid([a-z\\-0-9]+)',
component: new LoadableWrapper(() => import(/* webpackMode:"lazy", webpackChunkName: "accessions" */'accessions/ui/DisplayPage')),
component: Loadable({ loader: () => import(/* webpackMode:"lazy", webpackChunkName: "accessions" */'accessions/ui/DisplayPage') }),
exact: true,
},
{
// We need to take out the '10.' prefix for matching to work
path: '/10.:doi(\\d+\/.+)',
component: new LoadableWrapper(() => import(/* webpackMode:"lazy", webpackChunkName: "accessions" */'accessions/ui/DisplayPage')),
component: Loadable({ loader: () => import(/* webpackMode:"lazy", webpackChunkName: "accessions" */'accessions/ui/DisplayPage') }),
exact: true,
},
];
export {publicRoutes as accessionPublicRoutes};
export { publicRoutes as accessionPublicRoutes };
import Loading from 'ui/common/Loading';
import * as Loadable from 'react-loadable';
import Loadable from 'utilities/LoadableUtil';
const publicRoutes = [
{
path: '/content/news/:id/:slug',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/ActivityPostDisplayPage'),
loading: Loading,
}),
component: Loadable({ loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/ActivityPostDisplayPage') }),
},
{
path: '/content/:menuKey/:slug',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/ContentPage'),
loading: Loading,
}),
component: Loadable({ loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/ContentPage') }),
},
{
path: '/content/:slug',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/ContentPage'),
loading: Loading,
}),
component: Loadable({ loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/ContentPage') }),
},
{
path: '/documentation/:slug',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/DocumentationPage'),
loading: Loading,
}),
component: Loadable({ loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/DocumentationPage') }),
},
];
const adminRoutes = [
{
path: '/content/activity-post/:id/edit',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/admin/ActivityPostEditPage'),
loading: Loading,
}),
component: Loadable({ loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/admin/ActivityPostEditPage') }),
exact: true,
},
{
path: '/content/activity-post/edit',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/admin/ActivityPostEditPage'),
loading: Loading,
}),
component: Loadable({ loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/admin/ActivityPostEditPage') }),
exact: true,
},
{
path: '/content/activity-post/:filterCode(v.+)?',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/admin/ActivityPostBrowsePage'),
loading: Loading,
}),
component: Loadable({ loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/admin/ActivityPostBrowsePage') }),
},
{
path: '/content/:slug/edit',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/admin/EditPage'),
loading: Loading,
}),
component: Loadable({ loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/admin/EditPage') }),
exact: true,
},
{
path: '/content/:slug/edit/:className/:targetId',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/admin/EditPage'),
loading: Loading,
}),
component: Loadable({ loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/admin/EditPage') }),
exact: true,
},
{
path: '/content/edit',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/admin/EditPage'),
loading: Loading,
}),
component: Loadable({ loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/admin/EditPage') }),
exact: true,
},
{
path: '/content/:filterCode(v.+)?',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/admin/BrowsePage'),
loading: Loading,
}),
component: Loadable({ loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/admin/BrowsePage') }),
},
];
export {publicRoutes as cmsPublicRoutes, adminRoutes as cmsAdminRoutes};
export { publicRoutes as cmsPublicRoutes, adminRoutes as cmsAdminRoutes };
......@@ -37,8 +37,8 @@ class MenuStepper extends React.Component<IMenuStepperProps> {
return !menu ? null : (
<Grid item xs={ 3 } className={ classes.root }>
{ menu.items.map((menuItem: MenuItem) => (
<Link to={ menuItem.url }>
{ menu.items.map((menuItem: MenuItem, index) => (
<Link key={ `mi-${index}` } to={ menuItem.url }>
<div className={ classes.menuItem }>
{ t ? t(`cms.${menuItem.text}`) : menuItem.text }
</div>
......
import Loading from 'ui/common/Loading';
import * as Loadable from 'react-loadable';
import Loadable from 'utilities/LoadableUtil';
// Root routes
const rootRoutes = [
......@@ -8,7 +7,6 @@ const rootRoutes = [
path: '/c/',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "crop" */'crop/ui/BrowsePage'),
loading: Loading,
}),
exact: true,
},
......@@ -16,7 +14,6 @@ const rootRoutes = [
path: '/c/:shortName/edit',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "crop" */'crop/ui/EditPage'),
loading: Loading,
}),
exact: true,
},
......@@ -24,7 +21,6 @@ const rootRoutes = [
path: '/c/edit',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "crop" */'crop/ui/EditPage'),
loading: Loading,
}),
exact: true,
},
......@@ -32,10 +28,9 @@ const rootRoutes = [
path: '/c/:shortName',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "crop" */'crop/ui/DisplayPage'),
loading: Loading,
}),
exact: true,
},
];
export {rootRoutes as rootCropRoutes};
export { rootRoutes as rootCropRoutes };
import Loading from 'ui/common/Loading';
import * as Loadable from 'react-loadable';
import Loadable from 'utilities/LoadableUtil';
import steps from 'datasets/ui/dashboard/dataset-stepper/steps';
......@@ -10,7 +9,6 @@ const publicRoutes = [
path: '/datasets/suggest',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "datasets" */'datasets/ui/search/SuggestionsPage'),
loading: Loading,
}),
exact: true,
extraProps: {
......@@ -22,7 +20,6 @@ const publicRoutes = [
path: '/datasets/:filterCode(v.+)?',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "datasets" */'datasets/ui/BrowsePage'),
loading: Loading,
}),
exact: true,
extraProps: {
......@@ -34,7 +31,6 @@ const publicRoutes = [
path: '/datasets/:uuid([a-z\\-0-9]+)',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "datasets" */'datasets/ui/DisplayPage'),
loading: Loading,
}),
exact: true,
extraProps: {
......@@ -50,7 +46,6 @@ const dashboardRoutes = [
path: '/datasets/:filterCode(v.+)?',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "datasets" */'datasets/ui/dashboard/DashboardPage'),
loading: Loading,
}),
exact: true,
auth: [ROLE_USER, ROLE_ADMINISTRATOR],
......@@ -62,7 +57,6 @@ const dashboardRoutes = [
path: '/datasets/edit',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "datasets" */'datasets/ui/dashboard/StepperPage'),
loading: Loading,
}),
auth: [ROLE_USER, ROLE_ADMINISTRATOR],
extraProps: {
......@@ -73,7 +67,6 @@ const dashboardRoutes = [
path: '/',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "datasets" */'datasets/ui/dashboard/dataset-stepper/steps/basic-info'),
loading: Loading,
}),
exact: true,
},
......@@ -83,7 +76,6 @@ const dashboardRoutes = [
path: '/datasets/:uuid([a-z\\-0-9]+)/',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "datasets" */'datasets/ui/dashboard/StepperPage'),
loading: Loading,
}),
auth: [ROLE_USER, ROLE_ADMINISTRATOR],