Merge branch 'develop' into change-email

This commit is contained in:
William O'Beirne 2019-01-24 14:29:26 -05:00 committed by GitHub
commit 69fefdb2ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 277 additions and 399 deletions

View File

@ -8,32 +8,34 @@ import {
withRouter,
matchPath,
} from 'react-router';
import loadable from 'loadable-components';
import loadable from '@loadable/component';
import AuthRoute from 'components/AuthRoute';
import Template, { TemplateProps } from 'components/Template';
// wrap components in loadable...import & they will be split
const Home = loadable(() => import('pages/index'));
const Create = loadable(() => import('pages/create'));
const ProposalEdit = loadable(() => import('pages/proposal-edit'));
const Proposals = loadable(() => import('pages/proposals'));
const Proposal = loadable(() => import('pages/proposal'));
const opts = { fallback: <Loader size="large" /> };
const Home = loadable(() => import('pages/index'), opts);
const Create = loadable(() => import('pages/create'), opts);
const ProposalEdit = loadable(() => import('pages/proposal-edit'), opts);
const Proposals = loadable(() => import('pages/proposals'), opts);
const Proposal = loadable(() => import('pages/proposal'), opts);
const Auth = loadable(() => import('pages/auth'));
const SignOut = loadable(() => import('pages/sign-out'));
const Profile = loadable(() => import('pages/profile'));
const Settings = loadable(() => import('pages/settings'));
const Exception = loadable(() => import('pages/exception'));
const SignOut = loadable(() => import('pages/sign-out'), opts);
const Profile = loadable(() => import('pages/profile'), opts);
const Settings = loadable(() => import('pages/settings'), opts);
const Exception = loadable(() => import('pages/exception'), opts);
const Tos = loadable(() => import('pages/tos'));
const About = loadable(() => import('pages/about'));
const Privacy = loadable(() => import('pages/privacy'));
const Contact = loadable(() => import('pages/contact'));
const CodeOfConduct = loadable(() => import('pages/code-of-conduct'));
const VerifyEmail = loadable(() => import('pages/email-verify'));
const Callback = loadable(() => import('pages/callback'));
const RecoverEmail = loadable(() => import('pages/email-recover'));
const UnsubscribeEmail = loadable(() => import('pages/email-unsubscribe'));
const About = loadable(() => import('pages/about'), opts);
const Privacy = loadable(() => import('pages/privacy'), opts);
const Contact = loadable(() => import('pages/contact'), opts);
const CodeOfConduct = loadable(() => import('pages/code-of-conduct'), opts);
const VerifyEmail = loadable(() => import('pages/email-verify'), opts);
const Callback = loadable(() => import('pages/callback'), opts);
const RecoverEmail = loadable(() => import('pages/email-recover'), opts);
const UnsubscribeEmail = loadable(() => import('pages/email-unsubscribe'), opts);
import 'styles/style.less';
import Loader from 'components/Loader';
interface RouteConfig extends RouteProps {
route: RouteProps;
@ -284,9 +286,15 @@ class Routes extends React.PureComponent<Props> {
const routeComponents = routeConfigs.map(config => {
const { route, onlyLoggedIn, onlyLoggedOut } = config;
if (onlyLoggedIn || onlyLoggedOut) {
return <AuthRoute key={route.path} onlyLoggedOut={onlyLoggedOut} {...route} />;
return (
<AuthRoute
key={route.path as string}
onlyLoggedOut={onlyLoggedOut}
{...route}
/>
);
} else {
return <Route key={route.path} {...route} />;
return <Route key={route.path as string} {...route} />;
}
});

View File

@ -3,9 +3,9 @@ import { connect } from 'react-redux';
import { compose } from 'recompose';
import { withRouter, RouteComponentProps, Redirect } from 'react-router';
import { Switch, Route, Link } from 'react-router-dom';
import { Spin } from 'antd';
import { AppState } from 'store/reducers';
import { authActions } from 'modules/auth';
import Loader from 'components/Loader';
import Exception from 'pages/exception';
import SignIn from './SignIn';
import SignUp from './SignUp';
@ -65,7 +65,7 @@ class AuthFlow extends React.Component<Props> {
const { isCheckingUser, match } = this.props;
if (isCheckingUser) {
return <Spin size="large" />;
return <Loader size="large" />;
}
return (

View File

@ -1,9 +1,10 @@
import React from 'react';
import { connect } from 'react-redux';
import { Route, Redirect, RouteProps } from 'react-router-dom';
import { Spin, message } from 'antd';
import { Route, Redirect, RouteProps } from 'react-router';
import { message } from 'antd';
import { AppState } from 'store/reducers';
import { authActions } from 'modules/auth';
import Loader from 'components/Loader';
interface OwnProps {
onlyLoggedOut?: boolean;
@ -43,7 +44,7 @@ class AuthRoute extends React.Component<Props> {
...routeProps
} = this.props;
if (isCheckingUser) {
return <Spin tip="Checking authentication status" />;
return <Loader size="large" tip="Checking authentication status" />;
}
if ((user && !onlyLoggedOut) || (!user && onlyLoggedOut)) {
return <Route {...routeProps} />;

View File

@ -1,11 +1,12 @@
import React from 'react';
import classnames from 'classnames';
import { Form, Input, Spin, Button, Icon, Radio, message } from 'antd';
import { Form, Input, Button, Icon, Radio, message } from 'antd';
import { RadioChangeEvent } from 'antd/lib/radio';
import QRCode from 'qrcode.react';
import CopyToClipboard from 'react-copy-to-clipboard';
import { formatZcashURI, formatZcashCLI } from 'utils/formatters';
import { ContributionWithAddresses } from 'types';
import Loader from 'components/Loader';
import './PaymentInfo.less';
interface Props {
@ -73,7 +74,7 @@ export default class PaymentInfo extends React.Component<Props, State> {
<span style={{ opacity: uri ? 1 : 0 }}>
<QRCode value={uri || ''} />
</span>
{!uri && <Spin size="large" />}
{!uri && <Loader />}
</div>
<div className="PaymentInfo-uri-info">
<CopyInput

View File

@ -7,14 +7,6 @@
transform: translate(-50%, -50%);
text-align: center;
&-loader {
&-text {
opacity: 0.8;
font-size: 1.2rem;
margin-top: 1rem;
}
}
&-message {
display: flex;
justify-content: center;

View File

@ -1,7 +1,8 @@
import React from 'react';
import { connect } from 'react-redux';
import { Spin, Icon } from 'antd';
import { Icon } from 'antd';
import { Link } from 'react-router-dom';
import Loader from 'components/Loader';
import { createActions } from 'modules/create';
import { AppState } from 'store/reducers';
import './Final.less';
@ -56,12 +57,7 @@ class CreateFinal extends React.Component<Props> {
</div>
);
} else {
content = (
<div className="CreateFinal-loader">
<Spin size="large" />
<div className="CreateFinal-loader-text">Submitting your proposal...</div>
</div>
);
content = <Loader size="large" tip="Submitting your proposal..." />;
}
return <div className="CreateFinal">{content}</div>;

View File

@ -1,8 +1,9 @@
import React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { List, Button, Divider, Spin, Popconfirm, message } from 'antd';
import { Spin, List, Button, Divider, Popconfirm, message } from 'antd';
import Placeholder from 'components/Placeholder';
import Loader from 'components/Loader';
import { ProposalDraft, STATUS } from 'types';
import { fetchDrafts, createDraft, deleteDraft } from 'modules/create/actions';
import { AppState } from 'store/reducers';
@ -70,7 +71,7 @@ class DraftList extends React.Component<Props, State> {
const { deletingId } = this.state;
if (!drafts || isCreatingDraft) {
return <Spin />;
return <Loader size="large" />;
}
let draftsEl;

View File

@ -0,0 +1,34 @@
@import '~styles/variables.less';
.Loader {
position: relative;
color: @primary-color;
font-size: 2rem;
&:not(.is-inline) {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
&.is-large {
font-size: 3rem;
}
&.is-small {
font-size: 1.2rem;
}
&-tip {
position: absolute;
top: 100%;
left: 50%;
width: 200px;
margin-top: 0.5rem;
text-align: center;
color: @text-color-secondary;
font-size: 0.8rem;
transform: translateX(-50%);
}
}

View File

@ -0,0 +1,19 @@
import React from 'react';
import { Icon } from 'antd';
import classnames from 'classnames';
import './index.less';
interface Props {
size?: 'large' | 'small';
inline?: boolean;
tip?: string;
}
const Loader: React.SFC<Props> = ({ inline, size, tip }) => (
<div className={classnames('Loader', size && `is-${size}`, inline && 'is-inline')}>
<Icon type="loading" theme="outlined" />
{tip && <div className="Loader-tip">{tip}</div>}
</div>
);
export default Loader;

View File

@ -8,7 +8,7 @@ import {
} from 'react-router-dom';
import { connect } from 'react-redux';
import { compose } from 'recompose';
import { Spin, Tabs, Badge } from 'antd';
import { Tabs, Badge } from 'antd';
import { usersActions } from 'modules/users';
import { AppState } from 'store/reducers';
import HeaderDetails from 'components/HeaderDetails';
@ -20,6 +20,7 @@ import ProfileContribution from './ProfileContribution';
import ProfileComment from './ProfileComment';
import ProfileInvite from './ProfileInvite';
import Placeholder from 'components/Placeholder';
import Loader from 'components/Loader';
import Exception from 'pages/exception';
import ContributionModal from 'components/ContributionModal';
import LinkableTabs from 'components/LinkableTabs';
@ -50,6 +51,7 @@ class Profile extends React.Component<Props, State> {
componentDidMount() {
this.fetchData();
}
componentDidUpdate(prevProps: Props) {
const userLookupId = this.props.match.params.id;
const prevUserLookupId = prevProps.match.params.id;
@ -58,6 +60,7 @@ class Profile extends React.Component<Props, State> {
this.fetchData();
}
}
render() {
const { authUser, match, location } = this.props;
const { activeContribution } = this.state;
@ -76,7 +79,7 @@ class Profile extends React.Component<Props, State> {
const isAuthedUser = user && authUser && user.userid === authUser.userid;
if (waiting) {
return <Spin />;
return <Loader size="large" />;
}
if (user.fetchError) {

View File

@ -1,6 +1,6 @@
import React from 'react';
import moment from 'moment';
import { Spin, Form, Input, Button, Icon } from 'antd';
import { Form, Input, Button, Icon } from 'antd';
import { Proposal, STATUS } from 'types';
import classnames from 'classnames';
import { fromZat } from 'utils/units';
@ -10,6 +10,7 @@ import { AppState } from 'store/reducers';
import { withRouter } from 'react-router';
import UnitDisplay from 'components/UnitDisplay';
import ContributionModal from 'components/ContributionModal';
import Loader from 'components/Loader';
import { getAmountError } from 'utils/validators';
import { CATEGORY_UI } from 'api/constants';
import './style.less';
@ -167,7 +168,7 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
</React.Fragment>
);
} else {
content = <Spin />;
content = <Loader />;
}
return (

View File

@ -1,6 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import { Spin, Button, message } from 'antd';
import { Button, message } from 'antd';
import { AppState } from 'store/reducers';
import { Proposal } from 'types';
import { fetchProposalComments, postProposalComment } from 'modules/proposals/actions';
@ -12,6 +12,7 @@ import {
import { getIsSignedIn } from 'modules/auth/selectors';
import Comments from 'components/Comments';
import Placeholder from 'components/Placeholder';
import Loader from 'components/Loader';
import MarkdownEditor, { MARKDOWN_TYPE } from 'components/MarkdownEditor';
import './style.less';
@ -83,7 +84,7 @@ class ProposalComments extends React.Component<Props, State> {
let content = null;
if (isFetchingComments) {
content = <Spin />;
content = <Loader />;
} else if (commentsError) {
content = (
<>

View File

@ -1,9 +1,9 @@
import React from 'react';
import { connect } from 'react-redux';
import { Spin } from 'antd';
import UserRow from 'components/UserRow';
import Placeholder from 'components/Placeholder';
import UnitDisplay from 'components/UnitDisplay';
import Loader from 'components/Loader';
import { toZat } from 'utils/units';
import { fetchProposalContributions } from 'modules/proposals/actions';
import {
@ -83,7 +83,7 @@ class ProposalContributors extends React.Component<Props> {
} else if (fetchContributionsError) {
content = <Placeholder title="Something went wrong" subtitle={fetchContributionsError} />;
} else {
content = <Spin />;
content = <Loader />;
}
return (

View File

@ -1,9 +1,10 @@
import lodash from 'lodash';
import React from 'react';
import moment from 'moment';
import { Alert, Steps, Spin } from 'antd';
import { Alert, Steps } from 'antd';
import { Proposal, MILESTONE_STATE } from 'types';
import UnitDisplay from 'components/UnitDisplay';
import Loader from 'components/Loader';
import { AppState } from 'store/reducers';
import { connect } from 'react-redux';
import classnames from 'classnames';
@ -82,7 +83,7 @@ class ProposalMilestones extends React.Component<Props, State> {
render() {
const { proposal } = this.props;
if (!proposal) {
return <Spin />;
return <Loader />;
}
const { milestones } = proposal;

View File

@ -1,6 +1,6 @@
import React from 'react';
import { Spin } from 'antd';
import { Proposal } from 'types';
import Loader from 'components/Loader';
import UserRow from 'components/UserRow';
interface Props {
@ -12,7 +12,7 @@ const TeamBlock = ({ proposal }: Props) => {
if (proposal) {
content = proposal.team.map(user => <UserRow key={user.displayName} user={user} />);
} else {
content = <Spin />;
content = <Loader />;
}
return (

View File

@ -1,6 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import { Spin } from 'antd';
import Loader from 'components/Loader';
import Markdown from 'components/Markdown';
import moment from 'moment';
import Placeholder from 'components/Placeholder';
@ -58,7 +58,7 @@ class ProposalUpdates extends React.Component<Props, State> {
let content = null;
if (isFetchingUpdates) {
content = <Spin />;
content = <Loader />;
} else if (updatesError) {
content = <Placeholder title="Something went wrong" subtitle={updatesError} />;
} else if (updates) {

View File

@ -4,12 +4,13 @@ import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import Markdown from 'components/Markdown';
import LinkableTabs from 'components/LinkableTabs';
import Loader from 'components/Loader';
import { proposalActions } from 'modules/proposals';
import { bindActionCreators, Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import { Proposal, STATUS } from 'types';
import { getProposal } from 'modules/proposals/selectors';
import { Spin, Tabs, Icon, Dropdown, Menu, Button, Alert } from 'antd';
import { Tabs, Icon, Dropdown, Menu, Button, Alert } from 'antd';
import { AlertProps } from 'antd/lib/alert';
import CampaignBlock from './CampaignBlock';
import TeamBlock from './TeamBlock';
@ -93,7 +94,7 @@ export class ProposalDetail extends React.Component<Props, State> {
const showExpand = !isBodyExpanded && isBodyOverflowing;
if (!proposal) {
return <Spin />;
return <Loader size="large" />;
}
const deadline = 0; // TODO: Use actual date for deadline
@ -198,7 +199,7 @@ export class ProposalDetail extends React.Component<Props, State> {
{proposal ? (
<Markdown source={proposal.content} />
) : (
<Spin size="large" />
<Loader />
)}
</div>
{showExpand && (

View File

@ -1,6 +1,7 @@
import React from 'react';
import { AppState } from 'store/reducers';
import { Spin, Row, Col, Pagination } from 'antd';
import { Row, Col, Pagination } from 'antd';
import Loader from 'components/Loader';
import ProposalCard from '../ProposalCard';
interface Props {
@ -31,7 +32,7 @@ export default class ProposalResults extends React.Component<Props, State> {
const { page } = this.state;
if (isFetchingProposals) {
return <Spin size="large" />;
return <Loader size="large" />;
}
if (proposalsError) {

View File

@ -14,6 +14,7 @@
}
&-results {
position: relative;
flex: 1;
width: 100%;
}

View File

@ -4,7 +4,8 @@ import { AppState } from 'store/reducers';
import { updateUserSettings, getUserSettings } from 'api/api';
import { EmailSubscriptions as IEmailSubscriptions } from 'types';
import EmailSubscriptionsForm from 'components/EmailSubscriptionsForm';
import { Spin, message } from 'antd';
import { message } from 'antd';
import Loader from 'components/Loader';
interface StateProps {
authUser: AppState['auth']['user'];
@ -35,7 +36,7 @@ class EmailSubscriptions extends React.Component<Props, State> {
}
if (!emailSubscriptions) {
return <Spin />;
return <Loader />;
}
return (

View File

@ -6,6 +6,7 @@
min-height: 100vh;
&-content {
position: relative;
display: flex;
justify-content: center;
flex: 1;

View File

@ -2,12 +2,12 @@ import '@babel/polyfill';
import React from 'react';
import { hot } from 'react-hot-loader';
import { hydrate } from 'react-dom';
import { loadComponents } from 'loadable-components';
import { Provider } from 'react-redux';
import { Router } from 'react-router-dom';
import { PersistGate } from 'redux-persist/integration/react';
import * as Sentry from '@sentry/browser';
import { I18nextProvider } from 'react-i18next';
import { loadableReady } from '@loadable/component';
import { configureStore } from 'store/configure';
import history from 'store/history';
import { massageSerializedState } from 'utils/api';
@ -38,6 +38,6 @@ const App = hot(module)(() => (
</I18nextProvider>
));
loadComponents().then(() => {
loadableReady(() => {
hydrate(<App />, document.getElementById('app'));
});

View File

@ -1,11 +1,12 @@
import React from 'react';
import { Spin, Alert } from 'antd';
import { Alert } from 'antd';
import qs from 'query-string';
import { withRouter, RouteComponentProps, Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import { compose } from 'recompose';
import { AppState } from 'store/reducers';
import { SOCIAL_INFO } from 'utils/social';
import Loader from 'components/Loader';
interface StateProps {
authUser: AppState['auth']['user'];
@ -35,7 +36,7 @@ class Callback extends React.Component<Props, State> {
const { hasCheckedAuthUser, authUser } = this.props;
if (!hasCheckedAuthUser) {
return <Spin />;
return <Loader />;
}
if (hasCheckedAuthUser && !authUser) {

View File

@ -1,9 +1,10 @@
import React from 'react';
import { Spin, Button } from 'antd';
import { Button } from 'antd';
import qs from 'query-string';
import { withRouter, RouteComponentProps, Link } from 'react-router-dom';
import Result from 'ant-design-pro/lib/Result';
import { unsubscribeEmail } from 'api/api';
import Loader from 'components/Loader';
interface State {
isUnsubscribing: boolean;
@ -82,7 +83,7 @@ class UnsubscribeEmail extends React.Component<RouteComponentProps, State> {
/>
);
} else {
return <Spin size="large" />;
return <Loader size="large" />;
}
}
}

View File

@ -1,9 +1,10 @@
import React from 'react';
import { Spin, Button } from 'antd';
import { Button } from 'antd';
import qs from 'query-string';
import { withRouter, RouteComponentProps, Link } from 'react-router-dom';
import Result from 'ant-design-pro/lib/Result';
import { verifyEmail } from 'api/api';
import Loader from 'components/Loader';
interface State {
isVerifying: boolean;
@ -82,7 +83,7 @@ class VerifyEmail extends React.Component<RouteComponentProps, State> {
/>
);
} else {
return <Spin size="large" />;
return <Loader size="large" />;
}
}
}

View File

@ -26,7 +26,7 @@ interface Props {
code: '403' | '404' | '500';
}
const ExceptionComponent: React.SFC<Props> = ({ code }) => (
const ExceptionComponent = ({ code }: Props) => (
<Exception type={code} {...content[code]} />
);

View File

@ -1,10 +1,10 @@
import React from 'react';
import { connect } from 'react-redux';
import { withRouter, RouteComponentProps } from 'react-router';
import { Spin } from 'antd';
import CreateFlow from 'components/CreateFlow';
import { initializeForm } from 'modules/create/actions';
import { AppState } from 'store/reducers';
import Loader from 'components/Loader';
interface StateProps {
form: AppState['create']['form'];
@ -31,7 +31,7 @@ class ProposalEdit extends React.Component<Props> {
} else if (initializeFormError) {
return <h1>{initializeFormError}</h1>;
} else {
return <Spin />;
return <Loader />;
}
}
}

View File

@ -1,22 +0,0 @@
declare module 'loadable-components/server' {
import * as React from 'react';
export function getLoadableState(
rootElement: React.ReactElement<{}>,
rootContext?: any,
fetchRoot?: boolean,
tree?: any,
): Promise<DeferredState>;
export interface DeferredStateTree {
id: string;
children: DeferredStateTree[];
}
export interface DeferredState {
tree: DeferredStateTree;
getScriptContent(): string;
getScriptTag(): string;
getScriptElement(): React.ReactHTMLElement<HTMLScriptElement>;
}
}

View File

@ -23,7 +23,7 @@ const tsBabelLoaderClient = {
options: {
plugins: [
'dynamic-import-webpack', // for client
'loadable-components/babel',
'@loadable/babel-plugin',
'react-hot-loader/babel',
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-proposal-class-properties',
@ -47,7 +47,7 @@ const tsBabelLoaderServer = {
options: {
plugins: [
'dynamic-import-node', // for server
'loadable-components/babel',
'@loadable/babel-plugin',
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-proposal-class-properties',
['import', { libraryName: 'antd', style: false }],

View File

@ -6,6 +6,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ModuleDependencyWarning = require('./module-dependency-warning');
const WebappWebpackPlugin = require('webapp-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const LoadablePlugin = require('@loadable/webpack-plugin');
const env = require('../env')();
const paths = require('../paths');
@ -75,6 +76,7 @@ const client = [
return JSON.stringify(trans, null, 2);
},
}),
new LoadablePlugin(),
];
const server = [

View File

@ -39,6 +39,7 @@
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.0.0",
"@babel/register": "^7.0.0",
"@loadable/component": "5.5.0",
"@sentry/browser": "^4.3.2",
"@sentry/node": "^4.3.2",
"@svgr/webpack": "^2.4.0",
@ -59,7 +60,7 @@
"@types/react-dom": "16.0.9",
"@types/react-helmet": "^5.0.7",
"@types/react-redux": "^6.0.2",
"@types/react-router": "^4.0.31",
"@types/react-router": "4.4.3",
"@types/react-router-dom": "^4.3.1",
"@types/recompose": "^0.26.1",
"@types/redux-actions": "^2.3.0",
@ -110,7 +111,6 @@
"less": "^3.7.1",
"less-loader": "^4.1.0",
"lint-staged": "^7.2.2",
"loadable-components": "^2.2.3",
"lodash": "^4.17.10",
"markdown-loader": "^4.0.0",
"mini-css-extract-plugin": "^0.4.2",
@ -131,8 +131,8 @@
"react-i18next": "^8.3.5",
"react-mde": "^5.8.0",
"react-redux": "^5.0.7",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
"react-router": "4.4.0-beta.6",
"react-router-dom": "4.4.0-beta.6",
"recompose": "^0.27.1",
"redux": "^4.0.0",
"redux-devtools-extension": "^2.13.2",
@ -167,8 +167,13 @@
"xss": "1.0.3"
},
"devDependencies": {
"@loadable/babel-plugin": "5.5.0",
"@loadable/server": "5.5.0",
"@loadable/webpack-plugin": "5.5.0",
"@storybook/react": "4.0.0-alpha.22",
"@types/bn.js": "4.11.1",
"@types/loadable__component": "5.2.0",
"@types/loadable__server": "5.2.0",
"@types/qrcode.react": "^0.8.1",
"@types/query-string": "6.1.0",
"@types/react-copy-to-clipboard": "^4.2.6",

View File

@ -1,5 +1,6 @@
import * as React from 'react';
import Helmet from 'react-helmet';
import { ChunkExtractor } from '@loadable/server';
export interface Props {
children: any;
@ -9,7 +10,7 @@ export interface Props {
metaTags: Array<React.MetaHTMLAttributes<HTMLMetaElement>>;
state: string;
i18n: string;
loadableStateScript: string;
extractor: ChunkExtractor;
}
const HTML: React.SFC<Props> = ({
@ -20,7 +21,7 @@ const HTML: React.SFC<Props> = ({
i18n,
linkTags,
metaTags,
loadableStateScript,
extractor,
}) => {
const head = Helmet.renderStatic();
return (
@ -43,6 +44,7 @@ const HTML: React.SFC<Props> = ({
crossOrigin="anonymous"
/> */}
{/* Custom link & meta tags from webpack */}
{extractor.getLinkElements()}
{linkTags.map((l, idx) => (
<link key={idx} {...l as any} />
))}
@ -57,9 +59,12 @@ const HTML: React.SFC<Props> = ({
{head.link.toComponent()}
{head.script.toComponent()}
{extractor.getStyleElements()}
{css.map(href => {
return <link key={href} rel="stylesheet" href={href} />;
})}
{extractor.getScriptElements()}
<script
dangerouslySetInnerHTML={{
__html: `window.__PRELOADED_STATE__ = ${state}`,
@ -73,7 +78,6 @@ const HTML: React.SFC<Props> = ({
</head>
<body>
<div id="app" dangerouslySetInnerHTML={{ __html: children }} />
<script dangerouslySetInnerHTML={{ __html: loadableStateScript }} />
{scripts.map(src => {
return <script key={src} src={src} />;
})}

View File

@ -1,9 +1,8 @@
import fs from 'fs';
import path from 'path';
import React from 'react';
import { Request, Response } from 'express';
import { renderToString } from 'react-dom/server';
import { getLoadableState } from 'loadable-components/server';
import { ChunkExtractor } from '@loadable/server';
import { StaticRouter as Router } from 'react-router-dom';
import { Provider } from 'react-redux';
import { I18nextProvider } from 'react-i18next';
@ -19,63 +18,6 @@ import i18n from './i18n';
// @ts-ignore
import * as paths from '../config/paths';
import { storeActionsForPath } from './ssrAsync';
const isDev = process.env.NODE_ENV === 'development';
let cachedStats: any;
const getStats = () =>
new Promise((res, rej) => {
if (!isDev && cachedStats) {
res(cachedStats);
return;
}
const statsPath = path.join(paths.clientBuild, paths.publicPath, 'stats.json');
fs.readFile(statsPath, (e, d) => {
if (e) {
rej(e);
return;
}
cachedStats = JSON.parse(d.toString());
res(cachedStats);
});
});
const extractLoadableIds = (tree: any): string[] => {
const ids = (tree.id && [tree.id]) || [];
if (tree.children) {
return tree.children
.reduce((a: string[], c: any) => a.concat(extractLoadableIds(c)), [])
.concat(ids);
}
return ids;
};
// TODO: write tests for this
const chunkExtractFromLoadables = (loadableState: any) =>
getStats().then((stats: any) => {
const loadableIds = extractLoadableIds(loadableState.tree);
const mods = stats.modules.filter(
(m: any) =>
m.reasons.filter((r: any) => loadableIds.indexOf(r.userRequest) > -1).length > 0,
);
const chunks = mods.reduce((a: string[], m: any) => a.concat(m.chunks), []);
const origins = stats.chunks
.filter((c: any) => chunks.indexOf(c.id) > -1)
.map((c: any) => ({ loc: c.origins[0].loc, moduleId: c.origins[0].moduleId }));
const origin = origins[0];
const files = stats.chunks
.filter(
(c: any) =>
c.origins.filter(
(o: any) => origin && o.loc === origin.loc && o.moduleId === origin.moduleId,
).length > 0,
)
.reduce((a: string[], c: any) => a.concat(c.files), []);
return {
css: files.filter((f: string) => /.css$/.test(f)),
js: files.filter((f: string) => /.js$/.test(f)),
};
});
const serverRenderer = () => async (req: Request, res: Response) => {
const { store } = configureStore();
@ -98,18 +40,22 @@ const serverRenderer = () => async (req: Request, res: Response) => {
</I18nextProvider>
);
let loadableState;
let loadableFiles;
let extractor;
// 1. loadable state will render dynamic imports
try {
loadableState = await getLoadableState(reactApp);
loadableFiles = await chunkExtractFromLoadables(loadableState);
const statsFile = path.join(
paths.clientBuild,
paths.publicPath,
'loadable-stats.json',
);
extractor = new ChunkExtractor({ statsFile, entrypoints: ['bundle'] });
} catch (e) {
const disp = `Error getting loadable state for SSR`;
e.message = disp + ': ' + e.message;
log.error(e);
return res.status(500).send(disp + ' (more info in server logs)');
}
// 2. render and collect state
const content = renderToString(reactApp);
const state = JSON.stringify(store.getState());
@ -124,10 +70,10 @@ const serverRenderer = () => async (req: Request, res: Response) => {
return res.status(500).send(disp);
}
const cssFiles = ['bundle.css', 'vendor.css', ...loadableFiles.css]
const cssFiles = ['bundle.css', 'vendor.css']
.map(f => res.locals.assetPath(f))
.filter(Boolean);
const jsFiles = [...loadableFiles.js, 'vendor.js', 'bundle.js']
const jsFiles = ['vendor.js', 'bundle.js']
.map(f => res.locals.assetPath(f))
.filter(Boolean);
const mappedLinkTags = linkTags
@ -147,7 +93,7 @@ const serverRenderer = () => async (req: Request, res: Response) => {
metaTags={mappedMetaTags}
state={state}
i18n={i18nClient}
loadableStateScript={loadableState.getScriptContent()}
extractor={extractor}
>
{content}
</Html>,

File diff suppressed because it is too large Load Diff