Replace nextjs (#54)

* add new deps

* remove .babelrc

* add main files

* package scripts + add missing typings

* tslint ignore json

* replace next/router

* replace next/link

* HMR + configureStore + fontawsome header link

* Use Link instead of Redirect to solve same page redirect problem.

* Home svg import.

* hide filter button even if ant styles load first

* Integrate Helmet

* adjust style loading + fix font-face url format

* import style higher in render tree for improved SSR

* dev.js - nodemon only watch build/server dir

* precedence order fixed

* keep_fnames=true to keep uglifyjs from mangling BN

* small cleanup
This commit is contained in:
AMStrix 2018-09-17 15:55:49 -05:00 committed by Daniel Ternyak
parent cf8e621528
commit fe1e2a8df3
46 changed files with 4444 additions and 292 deletions

View File

@ -1,29 +0,0 @@
{
"presets": ["next/babel", "@zeit/next-typescript/babel"],
"env": {
"development": {
"plugins": ["inline-dotenv"]
},
"production": {
"plugins": ["transform-inline-environment-variables"]
}
},
"plugins": [
["import", { "libraryName": "antd", "style": false }],
[
"module-resolver",
{
"root": ["client"],
"extensions": [".js", ".tsx", ".ts"]
}
],
[
"styled-components",
{
"ssr": true,
"displayName": true,
"preprocess": false
}
]
]
}

29
frontend/bin/build.js Normal file
View File

@ -0,0 +1,29 @@
const webpack = require('webpack');
const rimraf = require('rimraf');
const webpackConfig = require('../config/webpack.config.js')(
process.env.NODE_ENV || 'production',
);
const paths = require('../config/paths');
const { logMessage } = require('./utils');
const build = async () => {
rimraf.sync(paths.clientBuild);
rimraf.sync(paths.serverBuild);
logMessage('Compiling, please wait...');
const [clientConfig, serverConfig] = webpackConfig;
const multiCompiler = webpack([clientConfig, serverConfig]);
multiCompiler.run((error, stats) => {
if (stats) {
console.log(stats.toString(clientConfig.stats));
}
if (error) {
logMessage('Compile error', error);
console.error(error);
}
});
};
build();

96
frontend/bin/dev.js Normal file
View File

@ -0,0 +1,96 @@
const fs = require('fs');
const webpack = require('webpack');
const nodemon = require('nodemon');
const rimraf = require('rimraf');
const webpackConfig = require('../config/webpack.config.js')(
process.env.NODE_ENV || 'development',
);
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');
const express = require('express');
const paths = require('../config/paths');
const truffleUtil = require('./truffle-util');
const { logMessage } = require('./utils');
const app = express();
const WEBPACK_PORT =
process.env.WEBPACK_PORT ||
(!isNaN(Number(process.env.PORT)) ? Number(process.env.PORT) + 1 : 3001);
const start = async () => {
rimraf.sync(paths.clientBuild);
rimraf.sync(paths.serverBuild);
await truffleUtil.ethereumCheck();
const [clientConfig, serverConfig] = webpackConfig;
clientConfig.entry.bundle = [
`webpack-hot-middleware/client?path=http://localhost:${WEBPACK_PORT}/__webpack_hmr`,
...clientConfig.entry.bundle,
];
clientConfig.output.hotUpdateMainFilename = 'updates/[hash].hot-update.json';
clientConfig.output.hotUpdateChunkFilename = 'updates/[id].[hash].hot-update.js';
const publicPath = clientConfig.output.publicPath;
clientConfig.output.publicPath = `http://localhost:${WEBPACK_PORT}${publicPath}`;
serverConfig.output.publicPath = `http://localhost:${WEBPACK_PORT}${publicPath}`;
const multiCompiler = webpack([clientConfig, serverConfig]);
const clientCompiler = multiCompiler.compilers[0];
const serverCompiler = multiCompiler.compilers[1];
serverCompiler.hooks.compile.tap('_', () => logMessage('Server compiling...', 'info'));
clientCompiler.hooks.compile.tap('_', () => logMessage('Client compiling...', 'info'));
const watchOptions = {
ignored: /node_modules/,
stats: clientConfig.stats,
};
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
return next();
});
const devMiddleware = webpackDevMiddleware(multiCompiler, {
publicPath: clientConfig.output.publicPath,
stats: clientConfig.stats,
watchOptions,
});
app.use(devMiddleware);
app.use(webpackHotMiddleware(clientCompiler));
app.use('/static', express.static(paths.clientBuild));
app.listen(WEBPACK_PORT);
// await first build...
await new Promise((res, rej) => devMiddleware.waitUntilValid(() => res()));
const script = nodemon({
script: `${paths.serverBuild}/server.js`,
watch: [paths.serverBuild],
verbose: true,
});
// uncomment to see nodemon details
// script.on('log', x => console.log(`LOG `, x.colour));
script.on('crash', () =>
logMessage(
'Server crashed, will attempt to restart after changes. Waiting...',
'error',
),
);
script.on('restart', () => {
logMessage('Server restarted.', 'warning');
});
script.on('error', () => {
logMessage('An error occured attempting to run the server. Exiting', 'error');
process.exit(1);
});
};
start();

View File

@ -0,0 +1,140 @@
const rimraf = require('rimraf');
const path = require('path');
const fs = require('fs');
const childProcess = require('child_process');
const Web3 = require('web3');
const paths = require('../config/paths');
const truffleConfig = require('../truffle');
const { logMessage } = require('./utils');
require('../config/env');
module.exports = {};
const clean = (module.exports.clean = () => {
rimraf.sync(paths.contractsBuild);
});
const compile = (module.exports.compile = () => {
childProcess.execSync('yarn build', { cwd: paths.contractsBase });
});
const migrate = (module.exports.migrate = () => {
childProcess.execSync('truffle migrate', { cwd: paths.contractsBase });
});
const makeWeb3Conn = () => {
const { host, port } = truffleConfig.networks.development;
return `ws://${host}:${port}`;
};
const createWeb3 = () => {
return new Web3(makeWeb3Conn());
};
const isGanacheUp = (module.exports.isGanacheUp = verbose =>
new Promise((res, rej) => {
verbose && logMessage(`Testing ganache @ ${makeWeb3Conn()}...`, 'info');
// console.log('curProv', web3.eth.currentProvider);
const web3 = createWeb3();
web3.eth.net
.isListening()
.then(() => {
verbose && logMessage('Ganache is UP!', 'info');
res(true);
web3.currentProvider.connection.close();
})
.catch(e => {
logMessage('Ganache appears to be down, unable to connect.', 'error');
res(false);
});
}));
const getGanacheNetworkId = (module.exports.getGanacheNetworkId = () => {
const web3 = createWeb3();
return web3.eth.net
.getId()
.then(id => {
web3.currentProvider.connection.close();
return id;
})
.catch(() => -1);
});
const checkContractsNetworkIds = (module.exports.checkContractsNetworkIds = id =>
new Promise((res, rej) => {
const buildDir = paths.contractsBuild;
fs.readdir(buildDir, (err, names) => {
if (err) {
logMessage(`No contracts build directory @ ${buildDir}`, 'error');
res(false);
} else {
const allHaveId = names.reduce((ok, name) => {
const contract = require(path.join(buildDir, name));
if (Object.keys(contract.networks).length > 0 && !contract.networks[id]) {
const actual = Object.keys(contract.networks).join(', ');
logMessage(`${name} should have networks[${id}], it has ${actual}`, 'error');
return false;
}
return true && ok;
}, true);
res(allHaveId);
}
});
}));
const fundWeb3v1 = (module.exports.fundWeb3v1 = () => {
// Fund ETH accounts
const ethAccounts = process.env.FUND_ETH_ADDRESSES
? process.env.FUND_ETH_ADDRESSES.split(',').map(a => a.trim())
: [];
const web3 = createWeb3();
return web3.eth.getAccounts().then(accts => {
if (ethAccounts.length) {
logMessage('Sending 50% of ETH balance from accounts...', 'info');
const txs = ethAccounts.map((addr, i) => {
return web3.eth
.getBalance(accts[i])
.then(parseInt)
.then(bal => {
const amount = '' + Math.round(bal / 2);
const amountEth = web3.utils.fromWei(amount);
return web3.eth
.sendTransaction({
to: addr,
from: accts[i],
value: amount,
})
.then(() => logMessage(` ${addr} <- ${amountEth} from ${accts[i]}`))
.catch(e =>
logMessage(` Error sending funds to ${addr} : ${e}`, 'error'),
);
});
});
return Promise.all(txs).then(() => web3.currentProvider.connection.close());
} else {
logMessage('No accounts specified for funding in .env file...', 'warning');
}
});
});
module.exports.ethereumCheck = () =>
isGanacheUp(true)
.then(isUp => !isUp && Promise.reject('network down'))
.then(getGanacheNetworkId)
.then(checkContractsNetworkIds)
.then(allHaveId => {
if (!allHaveId) {
logMessage('Contract problems, will compile & migrate.', 'warning');
clean();
logMessage('truffle compile, please wait...', 'info');
compile();
logMessage('truffle migrate, please wait...', 'info');
migrate();
fundWeb3v1();
} else {
logMessage('OK, Contracts have correct network id.', 'info');
}
})
.catch(e => logMessage('WARNING: ethereum setup has a problem: ' + e, 'error'));

27
frontend/bin/utils.js Normal file
View File

@ -0,0 +1,27 @@
const chalk = require('chalk');
const net = require('net');
const logMessage = (message, level = 'info') => {
const colors = { error: 'red', warning: 'yellow', info: 'blue' };
colors[undefined] = 'white';
console.log(`${chalk[colors[level]](message)}`);
};
const isPortTaken = port =>
new Promise((res, rej) => {
const tester = net
.createServer()
.once('error', function(err) {
err.code != 'EADDRINUSE' && rej();
err.code == 'EADDRINUSE' && res(true);
})
.once('listening', () => {
tester.once('close', () => res(false)).close();
})
.listen(port, '127.0.0.1');
});
module.exports = {
logMessage,
isPortTaken,
};

View File

@ -0,0 +1,50 @@
import React from 'react';
import { hot } from 'react-hot-loader';
import { Switch, Route, Redirect } from 'react-router';
import { injectGlobal } from 'styled-components';
import loadable from 'loadable-components';
// wrap components in loadable...import & they will be split
const Home = loadable(() => import('pages/index'));
const Create = loadable(() => import('pages/create'));
const Proposals = loadable(() => import('pages/proposals'));
const Proposal = loadable(() => import('pages/proposal'));
import 'styles/style.less';
// tslint:disable-next-line:no-unused-expression
injectGlobal`
* {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
}
html {
font-size: 16px;
@media (max-width: 900px) {
font-size: 14px;
}
@media (max-width: 600px) {
font-size: 12px;
}
}
`;
class Routes extends React.Component<any> {
render() {
return (
<Switch>
<Route exact path="/" component={Home} />
<Route path="/create" component={Create} />
<Route exact path="/proposals" component={Proposals} />
<Route path="/proposals/:id" component={Proposal} />
<Route path="/*" render={() => <Redirect to="/" />} />
</Switch>
);
}
}
export default hot(module)(Routes);

View File

@ -1,7 +1,5 @@
import React from 'react';
import Head from 'next/head';
import 'styles/style.less';
import { Helmet } from 'react-helmet';
interface Props {
title: string;
@ -12,20 +10,16 @@ export default class BasicHead extends React.Component<Props> {
const { children, title } = this.props;
return (
<div>
<Head>
<Helmet>
<title>Grant.io - {title}</title>
{/*TODO - bundle*/}
<meta name={`${title} page`} content={`${title} page stuff`} />
<link
rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.2.0/css/all.css"
integrity="sha384-hWVjflwFxL6sNzntih27bfxkr27PmbbK/iSvJ+a4+0owXq79v+lsFkW54bOGbiDQ"
crossOrigin="anonymous"
/>
<link rel="stylesheet" href="/_next/static/style.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
</Helmet>
{children}
</div>
);

View File

@ -1,5 +1,5 @@
import React from 'react';
import Link from 'next/link';
import { Link } from 'react-router-dom';
import { Icon } from 'antd';
import * as Styled from './styled';
@ -16,10 +16,8 @@ const CreateSuccess = ({ crowdFundCreatedAddress }: Props) => (
<h2>Contract was succesfully deployed!</h2>
<div>
Your proposal is now live and on the blockchain!{' '}
<Link href={`/proposals/${crowdFundCreatedAddress}`}>
<a>Click here</a>
</Link>{' '}
to check it out.
<Link to={`/proposals/${crowdFundCreatedAddress}`}>Click here</Link> to check it
out.
</div>
</Styled.SuccessText>
</Styled.Success>

View File

@ -1,3 +1,4 @@
import React from 'react';
import { Input, DatePicker, Card, Icon, Alert, Checkbox } from 'antd';
import moment from 'moment';

View File

@ -1,12 +1,12 @@
import React from 'react';
import Link from 'next/link';
import { Link } from 'react-router-dom';
import * as Styled from './styled';
export default () => (
<Styled.Footer>
<Link href="/">
<Styled.Title>Grant.io</Styled.Title>
</Link>
<Styled.Title>
<Link to="/">Grant.io</Link>
</Styled.Title>
{/*<Styled.Links>
<Styled.Link>about</Styled.Link>
<Styled.Link>legal</Styled.Link>

View File

@ -10,18 +10,20 @@ export const Footer = styled.footer`
height: 140px;
`;
export const Title = styled.a`
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: #fff;
transition: transform 100ms ease;
export const Title = styled.span`
a {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: #fff;
transition: transform 100ms ease;
&:hover,
&:focus,
&:active {
transform: translateY(-1px);
color: inherit;
&:hover,
&:focus,
&:active {
transform: translateY(-1px);
color: inherit;
}
}
`;

View File

@ -1,6 +1,6 @@
import React from 'react';
import { Icon } from 'antd';
import Link from 'next/link';
import { Link } from 'react-router-dom';
import * as Styled from './styled';
interface OwnProps {
@ -15,30 +15,28 @@ export default class Header extends React.Component<Props> {
return (
<React.Fragment>
<Styled.Header isTransparent={isTransparent}>
<div style={{ display: 'flex' }}>
<Link href="/proposals">
<Styled.Button>
<Styled.ButtonIcon>
<Icon type="shop" />
</Styled.ButtonIcon>
<Styled.ButtonText>Explore</Styled.ButtonText>
</Styled.Button>
<Styled.Button style={{ display: 'flex' }}>
<Link to="/proposals">
<Styled.ButtonIcon>
<Icon type="shop" />
</Styled.ButtonIcon>
<Styled.ButtonText>Explore</Styled.ButtonText>
</Link>
</div>
</Styled.Button>
<Link href="/">
<Styled.Title>Grant.io</Styled.Title>
</Link>
<Styled.Title>
<Link to="/">Grant.io</Link>
</Styled.Title>
<React.Fragment>
<Link href="/create">
<Styled.Button style={{ marginLeft: '1.5rem' }}>
<Styled.Button style={{ marginLeft: '1.5rem' }}>
<Link to="/create">
<Styled.ButtonIcon>
<Icon type="form" />
</Styled.ButtonIcon>
<Styled.ButtonText>Start a Proposal</Styled.ButtonText>
</Styled.Button>
</Link>
</Link>
</Styled.Button>
</React.Fragment>
{!isTransparent && <Styled.AlphaBanner>Alpha</Styled.AlphaBanner>}

View File

@ -24,44 +24,48 @@ export const Header = styled<{ isTransparent: boolean }, 'header'>('header')`
box-shadow: ${(p: any) => (p.isTransparent ? 'none' : '0 1px 2px rgba(0, 0, 0, 0.3)')};
`;
export const Title = styled.a`
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
font-size: 2.2rem;
margin: 0;
color: inherit;
letter-spacing: 0.08rem;
font-weight: 500;
transition: transform 100ms ease;
flex-grow: 1;
text-align: center;
&:hover,
&:focus,
&:active {
export const Title = styled.span`
a {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
font-size: 2.2rem;
margin: 0;
color: inherit;
transform: translateY(-2px) translate(-50%, -50%);
letter-spacing: 0.08rem;
font-weight: 500;
transition: transform 100ms ease;
flex-grow: 1;
text-align: center;
&:hover,
&:focus,
&:active {
color: inherit;
transform: translateY(-2px) translate(-50%, -50%);
}
}
`;
export const Button = styled.a`
display: block;
background: none;
padding: 0;
font-size: 1.2rem;
font-weight: 300;
color: inherit;
letter-spacing: 0.05rem;
cursor: pointer;
transition: transform 100ms ease;
&:hover,
&:focus,
&:active {
transform: translateY(-1px);
export const Button = styled.div`
a {
display: block;
background: none;
padding: 0;
font-size: 1.2rem;
font-weight: 300;
color: inherit;
letter-spacing: 0.05rem;
cursor: pointer;
transition: transform 100ms ease;
&:hover,
&:focus,
&:active {
transform: translateY(-1px);
color: inherit;
}
}
`;

View File

@ -1,26 +1,33 @@
import React from 'react';
import * as Styled from './styled';
import Link from 'next/link';
import { Redirect } from 'react-router-dom';
import { Icon } from 'antd';
import AntWrap from 'components/AntWrap';
import TeamsSvg from 'static/images/intro-teams.svg';
import FundingSvg from 'static/images/intro-funding.svg';
import CommunitySvg from 'static/images/intro-community.svg';
const introBlobs = [
{
image: 'static/images/intro-teams.svg',
Svg: TeamsSvg,
text: 'Developers and teams propose projects for improving the ecosystem',
},
{
image: 'static/images/intro-funding.svg',
Svg: FundingSvg,
text: 'Projects are funded by the community and paid as its built',
},
{
image: 'static/images/intro-community.svg',
Svg: CommunitySvg,
text: 'Open discussion and project updates bring devs and the community together',
},
];
export default class Home extends React.Component {
state = { redirect: '' };
render() {
if (this.state.redirect) {
return <Redirect push to={this.state.redirect} />;
}
return (
<AntWrap title="Home" isHeaderTransparent isFullScreen>
<Styled.Hero>
@ -29,12 +36,15 @@ export default class Home extends React.Component {
</Styled.HeroTitle>
<Styled.HeroButtons>
<Link href="/create">
<Styled.HeroButton isPrimary>Propose a Project</Styled.HeroButton>
</Link>
<Link href="/proposals">
<Styled.HeroButton>Explore Projects</Styled.HeroButton>
</Link>
<Styled.HeroButton
onClick={() => this.setState({ redirect: '/create' })}
isPrimary
>
Propose a Project
</Styled.HeroButton>
<Styled.HeroButton onClick={() => this.setState({ redirect: '/proposals' })}>
Explore Projects
</Styled.HeroButton>
</Styled.HeroButtons>
<Styled.HeroScroll>
@ -52,7 +62,7 @@ export default class Home extends React.Component {
<Styled.IntroBlobs>
{introBlobs.map((blob, i) => (
<Styled.IntroBlob key={i}>
<img src={blob.image} />
<blob.Svg />
<p>{blob.text}</p>
</Styled.IntroBlob>
))}

View File

@ -142,7 +142,7 @@ export const IntroBlob = styled.div`
max-width: 320px;
}
img {
svg {
margin-bottom: 1rem;
opacity: 0.75;
height: 100px;

View File

@ -9,7 +9,7 @@ import { connect } from 'react-redux';
import { compose } from 'recompose';
import { AppState } from 'store/reducers';
import { web3Actions } from 'modules/web3';
import { withRouter } from 'next/router';
import { withRouter } from 'react-router';
import Web3Container, { Web3RenderProps } from 'lib/Web3Container';
import ShortAddress from 'components/ShortAddress';
import UnitDisplay from 'components/UnitDisplay';

View File

@ -18,7 +18,7 @@ import GovernanceTab from './Governance';
import ContributorsTab from './Contributors';
// import CommunityTab from './Community';
import * as Styled from './styled';
import { withRouter } from 'next/router';
import { withRouter } from 'react-router';
import Web3Container from 'lib/Web3Container';
import { web3Actions } from 'modules/web3';

View File

@ -1,7 +1,7 @@
import React from 'react';
import { Progress, Icon, Spin } from 'antd';
import moment from 'moment';
import Link from 'next/link';
import { Redirect } from 'react-router-dom';
import { CATEGORY_UI } from 'api/constants';
import { ProposalWithCrowdFund } from 'modules/proposals/reducers';
import * as Styled from './styled';
@ -18,7 +18,11 @@ interface Props extends ProposalWithCrowdFund {
}
class ProposalCard extends React.Component<Props> {
state = { redirect: '' };
render() {
if (this.state.redirect) {
return <Redirect push to={this.state.redirect} />;
}
const { title, proposalId, category, dateCreated, web3, crowdFund } = this.props;
const team = [...this.props.team].reverse();
@ -26,48 +30,47 @@ class ProposalCard extends React.Component<Props> {
return <Spin />;
} else {
return (
<Link href={`/proposals/${proposalId}`}>
<Styled.Container>
<Styled.Title>{title}</Styled.Title>
<Styled.Funding>
<Styled.FundingRaised>
<UnitDisplay value={crowdFund.funded} symbol="ETH" />{' '}
<small>raised</small> of{' '}
<UnitDisplay value={crowdFund.target} symbol="ETH" /> goal
</Styled.FundingRaised>
<Styled.FundingPercent isFunded={crowdFund.percentFunded >= 100}>
{crowdFund.percentFunded}%
</Styled.FundingPercent>
</Styled.Funding>
<Progress
percent={crowdFund.percentFunded}
status={crowdFund.percentFunded >= 100 ? 'success' : 'active'}
showInfo={false}
/>
<Styled.Container
onClick={() => this.setState({ redirect: `/proposals/${proposalId}` })}
>
<Styled.Title>{title}</Styled.Title>
<Styled.Funding>
<Styled.FundingRaised>
<UnitDisplay value={crowdFund.funded} symbol="ETH" /> <small>raised</small>{' '}
of <UnitDisplay value={crowdFund.target} symbol="ETH" /> goal
</Styled.FundingRaised>
<Styled.FundingPercent isFunded={crowdFund.percentFunded >= 100}>
{crowdFund.percentFunded}%
</Styled.FundingPercent>
</Styled.Funding>
<Progress
percent={crowdFund.percentFunded}
status={crowdFund.percentFunded >= 100 ? 'success' : 'active'}
showInfo={false}
/>
<Styled.Team>
<Styled.TeamName>
{team[0].accountAddress}{' '}
{team.length > 1 && <small>+{team.length - 1} other</small>}
</Styled.TeamName>
<Styled.TeamAvatars>
{team.reverse().map(u => (
<Identicon key={u.userid} address={u.accountAddress} />
))}
</Styled.TeamAvatars>
</Styled.Team>
<Styled.ContractAddress>{proposalId}</Styled.ContractAddress>
<Styled.Team>
<Styled.TeamName>
{team[0].accountAddress}{' '}
{team.length > 1 && <small>+{team.length - 1} other</small>}
</Styled.TeamName>
<Styled.TeamAvatars>
{team.reverse().map(u => (
<Identicon key={u.userid} address={u.accountAddress} />
))}
</Styled.TeamAvatars>
</Styled.Team>
<Styled.ContractAddress>{proposalId}</Styled.ContractAddress>
<Styled.Info>
<Styled.InfoCategory style={{ color: CATEGORY_UI[category].color }}>
<Icon type={CATEGORY_UI[category].icon} /> {CATEGORY_UI[category].label}
</Styled.InfoCategory>
<Styled.InfoCreated>
{moment(dateCreated * 1000).fromNow()}
</Styled.InfoCreated>
</Styled.Info>
</Styled.Container>
</Link>
<Styled.Info>
<Styled.InfoCategory style={{ color: CATEGORY_UI[category].color }}>
<Icon type={CATEGORY_UI[category].icon} /> {CATEGORY_UI[category].label}
</Styled.InfoCategory>
<Styled.InfoCreated>
{moment(dateCreated * 1000).fromNow()}
</Styled.InfoCreated>
</Styled.Info>
</Styled.Container>
);
}
}

25
frontend/client/index.tsx Normal file
View File

@ -0,0 +1,25 @@
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 { BrowserRouter as Router } from 'react-router-dom';
import { configureStore } from 'store/configure';
import Routes from './Routes';
const initialState = window && (window as any).__PRELOADED_STATE__;
const store = configureStore(initialState);
const App = hot(module)(() => (
<Provider store={store}>
<Router>
<Routes />
</Router>
</Provider>
));
loadComponents().then(() => {
hydrate(<App />, document.getElementById('app'));
});

View File

@ -2,16 +2,16 @@ import React, { Component } from 'react';
import Web3Page from 'components/Web3Page';
import Proposal from 'components/Proposal';
import { WithRouterProps, withRouter } from 'next/router';
import { withRouter, RouteComponentProps } from 'react-router';
type RouteProps = WithRouterProps;
type RouteProps = RouteComponentProps<any>;
class ProposalPage extends Component<RouteProps> {
constructor(props: RouteProps) {
super(props);
}
render() {
const proposalId = this.props.router.query.id as string;
const proposalId = this.props.match.params.id;
return (
<Web3Page
title={`Proposal ${proposalId}`}

View File

@ -35,5 +35,13 @@ export function configureStore(
// };
// store.runSagaTask();
if (process.env.NODE_ENV === 'development') {
if (module.hot) {
module.hot.accept('./reducers', () =>
store.replaceReducer(require('./reducers').default),
);
}
}
return store;
}

View File

@ -1,17 +1,17 @@
@font-face {
font-family: 'Nunito Sans';
font-weight: 400;
src: url('/_next/static/fonts/NunitoSans-Regular.ttf') format('ttf');
src: url('../static/fonts/NunitoSans-Regular.ttf') format('truetype');
}
@font-face {
font-family: 'Nunito Sans';
font-weight: 600;
src: url('/_next/static/fonts/NunitoSans-SemiBold.ttf') format('ttf');
src: url('../static/fonts/NunitoSans-SemiBold.ttf') format('truetype');
}
@font-face {
font-family: 'Nunito Sans';
font-weight: 300;
src: url('/_next/static/fonts/NunitoSans-Light.ttf') format('ttf');
src: url('../static/fonts/NunitoSans-Light.ttf') format('truetype');
}

View File

@ -0,0 +1 @@
declare module 'express-manifest-helpers';

View File

@ -0,0 +1 @@
declare module 'loadable-components/server';

52
frontend/config/env.js Normal file
View File

@ -0,0 +1,52 @@
const fs = require('fs');
const path = require('path');
const paths = require('./paths');
delete require.cache[require.resolve('./paths')];
if (!process.env.NODE_ENV) {
throw new Error(
'The process.env.NODE_ENV environment variable is required but was not specified.',
);
}
// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
const dotenvFiles = [
`${paths.dotenv}.${process.env.NODE_ENV}.local`,
`${paths.dotenv}.${process.env.NODE_ENV}`,
process.env.NODE_ENV !== 'test' && `${paths.dotenv}.local`,
paths.dotenv,
].filter(Boolean);
dotenvFiles.forEach(dotenvFile => {
if (fs.existsSync(dotenvFile)) {
require('dotenv').config({
path: dotenvFile,
});
}
});
const appDirectory = fs.realpathSync(process.cwd());
process.env.NODE_PATH = (process.env.NODE_PATH || '')
.split(path.delimiter)
.filter(folder => folder && !path.isAbsolute(folder))
.map(folder => path.resolve(appDirectory, folder))
.join(path.delimiter);
module.exports = () => {
const raw = {
PORT: process.env.PORT || 3000,
NODE_ENV: process.env.NODE_ENV || 'development',
BACKEND_URL: process.env.BACKEND_URL || 'http://localhost:5000',
};
// Stringify all values so we can feed into Webpack DefinePlugin
const stringified = {
'process.env': Object.keys(raw).reduce((env, key) => {
env[key] = JSON.stringify(raw[key]);
return env;
}, {}),
};
return { raw, stringified };
};

24
frontend/config/paths.js Normal file
View File

@ -0,0 +1,24 @@
const path = require('path');
const fs = require('fs');
const findRoot = require('find-root');
const appDirectory = fs.realpathSync(process.cwd());
// truffle exec cwd moves to called js, so make sure we are on root
const appRoot = findRoot(appDirectory);
const resolveApp = relativePath => path.resolve(appRoot, relativePath);
const paths = {
clientBuild: resolveApp('build/client'),
contractsBase: resolveApp('../contract'),
contractsBuild: resolveApp('../contract/build/contracts'),
dotenv: resolveApp('.env'),
logs: resolveApp('logs'),
publicPath: '/static/',
serverBuild: resolveApp('build/server'),
srcClient: resolveApp('client'),
srcServer: resolveApp('server'),
};
paths.resolveModules = [paths.srcClient, paths.srcServer, 'node_modules'];
module.exports = paths;

View File

@ -0,0 +1,74 @@
const path = require('path');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const paths = require('../paths');
const { client: clientLoaders } = require('./loaders');
const resolvers = require('./resolvers');
const plugins = require('./plugins');
const isDev = process.env.NODE_ENV === 'development';
module.exports = {
name: 'client',
target: 'web',
entry: {
bundle: [path.join(paths.srcClient, 'index.tsx')],
},
output: {
path: path.join(paths.clientBuild, paths.publicPath),
filename: 'bundle.js',
publicPath: paths.publicPath,
chunkFilename: isDev ? '[name].chunk.js' : '[name].[chunkhash:8].chunk.js',
},
module: {
rules: clientLoaders,
},
resolve: { ...resolvers },
plugins: [...plugins.shared, ...plugins.client],
node: {
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty',
},
optimization: {
minimizer: [
new UglifyJsPlugin({
cache: true,
parallel: true,
sourceMap: true,
uglifyOptions: {
// otherwise BN typecheck gets mangled during minification
keep_fnames: true,
},
}),
],
namedModules: true,
noEmitOnErrors: false,
// concatenateModules: true,
// below settings bundle all vendor css in one file
// this allows SSR to render a reference to the hashed css
// if commons is split by module then flickering may occur on load
splitChunks: {
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all',
},
},
},
},
stats: {
cached: false,
cachedAssets: false,
chunks: false,
chunkModules: false,
colors: true,
hash: false,
modules: false,
reasons: false,
timings: true,
version: false,
},
};

View File

@ -0,0 +1,28 @@
const baseConfig = require('./client.base');
const webpack = require('webpack');
const WriteFileWebpackPlugin = require('write-file-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const config = {
...baseConfig,
plugins: [
new WriteFileWebpackPlugin(),
new webpack.HotModuleReplacementPlugin(),
...baseConfig.plugins,
new ForkTsCheckerWebpackPlugin(),
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: '../../dev-client-bundle-analysis.html',
defaultSizes: 'gzip',
openAnalyzer: false,
}),
],
mode: 'development',
devtool: 'cheap-module-inline-source-map',
performance: {
hints: false,
},
};
module.exports = config;

View File

@ -0,0 +1,21 @@
const baseConfig = require('./client.base');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const config = {
...baseConfig,
plugins: [
...baseConfig.plugins,
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: '../../prod-client-bundle-analysis.html',
defaultSizes: 'gzip',
openAnalyzer: false,
}),
],
mode: 'production',
devtool: 'source-map',
};
config.output.filename = 'bundle.[hash:8].js';
module.exports = config;

View File

@ -0,0 +1,8 @@
module.exports = (env = 'production') => {
if (env === 'development' || env === 'dev') {
process.env.NODE_ENV = 'development';
return [require('./client.dev'), require('./server.dev')];
}
process.env.NODE_ENV = 'production';
return [require('./client.prod'), require('./server.prod')];
};

View File

@ -0,0 +1,229 @@
const _ = require('lodash');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const isDev = process.env.NODE_ENV === 'development';
const babelPresets = [
'@babel/react',
// '@babel/typescript', (using ts-loader)
['@babel/env', { useBuiltIns: 'entry', modules: false }],
];
const lessLoader = {
loader: 'less-loader',
options: { javascriptEnabled: true },
};
const tsBabelLoaderClient = {
test: /\.tsx?$/,
use: [
{
loader: 'babel-loader',
options: {
plugins: [
['styled-components', { ssr: true, displayName: true }],
'dynamic-import-webpack', // for client
'loadable-components/babel',
'react-hot-loader/babel',
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-proposal-class-properties',
['import', { libraryName: 'antd', style: false }],
],
presets: ['@babel/react', ['@babel/env', { useBuiltIns: 'entry' }]],
},
},
{
loader: 'ts-loader',
options: { transpileOnly: isDev },
},
],
};
const tsBabelLoaderServer = {
test: /\.tsx?$/,
use: [
{
loader: 'babel-loader',
options: {
plugins: [
['styled-components', { ssr: true, displayName: true }],
'dynamic-import-node', // for server
'loadable-components/babel',
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-proposal-class-properties',
['import', { libraryName: 'antd', style: false }],
],
presets: [
'@babel/react',
['@babel/env', { useBuiltIns: 'entry', targets: { node: 'current' } }],
],
},
},
{
loader: 'ts-loader',
options: { transpileOnly: isDev },
},
],
};
const cssLoaderClient = {
test: /\.css$/,
exclude: [/node_modules/],
use: [
isDev && 'style-loader',
!isDev && MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
},
].filter(Boolean),
};
const lessLoaderClient = {
test: /\.less$/,
exclude: [/node_modules/],
use: [...cssLoaderClient.use, lessLoader],
};
const cssLoaderServer = {
test: /\.css$/,
exclude: [/node_modules/],
use: [
{
loader: 'css-loader/locals',
},
],
};
const lessLoaderServer = {
test: /\.less$/,
exclude: [/node_modules/],
use: [...cssLoaderServer.use, lessLoader],
};
const urlLoaderClient = {
test: /\.(png|jpe?g|gif)$/,
loader: require.resolve('url-loader'),
options: {
limit: 2048,
name: 'assets/[name].[hash:8].[ext]',
},
};
const urlLoaderServer = {
...urlLoaderClient,
options: {
...urlLoaderClient.options,
emitFile: false,
},
};
const fileLoaderClient = {
// WARNING: this will catch all files except those below
exclude: [/\.(js|ts|tsx|css|less|mjs|html|json|ejs)$/],
use: [
{
loader: 'file-loader',
options: {
name: 'assets/[name].[hash:8].[ext]',
},
},
],
};
const fileLoaderServer = _.defaultsDeep(
{
use: [{ options: { emitFile: false } }],
},
fileLoaderClient,
);
const svgLoaderClient = {
test: /\.svg$/,
issuer: {
test: /\.tsx?$/,
},
use: [
{
loader: '@svgr/webpack',
options: {
svgoConfig: {
plugins: [{ inlineStyles: { onlyMatchedOnce: false } }],
},
},
},
], // svg -> react component
};
const svgLoaderServer = svgLoaderClient;
// Write css files from node_modules to its own vendor.css file
const externalCssLoaderClient = {
test: /\.css$/,
include: [/node_modules/],
use: [
isDev && 'style-loader',
!isDev && MiniCssExtractPlugin.loader,
'css-loader',
].filter(Boolean),
};
const externalLessLoaderClient = {
test: /\.less$/,
include: [/node_modules/],
use: [
isDev && 'style-loader',
!isDev && MiniCssExtractPlugin.loader,
'css-loader',
lessLoader,
].filter(Boolean),
};
// Server build needs a loader to handle external .css files
const externalCssLoaderServer = {
test: /\.css$/,
include: [/node_modules/],
loader: 'css-loader/locals',
};
const externalLessLoaderServer = {
test: /\.less$/,
include: [/node_modules/],
use: ['css-loader/locals', lessLoader],
};
const client = [
{
// oneOf: first matching rule takes all
oneOf: [
tsBabelLoaderClient,
cssLoaderClient,
lessLoaderClient,
svgLoaderClient,
urlLoaderClient,
fileLoaderClient,
externalCssLoaderClient,
externalLessLoaderClient,
],
},
];
const server = [
{
// oneOf: first matching rule takes all
oneOf: [
tsBabelLoaderServer,
cssLoaderServer,
lessLoaderServer,
svgLoaderServer,
urlLoaderServer,
fileLoaderServer,
externalCssLoaderServer,
externalLessLoaderServer,
],
},
];
module.exports = {
client,
server,
};

View File

@ -0,0 +1,24 @@
const ModuleDependencyWarning = require('webpack/lib/ModuleDependencyWarning');
// supress unfortunate warnings due to transpileOnly=true and certain ts export patterns
// https://github.com/TypeStrong/ts-loader/issues/653#issuecomment-390889335
// https://github.com/TypeStrong/ts-loader/issues/751
module.exports = class IgnoreNotFoundExportPlugin {
apply(compiler) {
const messageRegExp = /export '.*'( \(reexported as '.*'\))? was not found in/;
function doneHook(stats) {
stats.compilation.warnings = stats.compilation.warnings.filter(function(warn) {
if (warn instanceof ModuleDependencyWarning && messageRegExp.test(warn.message)) {
return false;
}
return true;
});
}
if (compiler.hooks) {
compiler.hooks.done.tap('IgnoreNotFoundExportPlugin', doneHook);
} else {
compiler.plugin('done', doneHook);
}
}
};

View File

@ -0,0 +1,60 @@
const webpack = require('webpack');
const ManifestPlugin = require('webpack-manifest-plugin');
const { StatsWriterPlugin } = require('webpack-stats-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ModuleDependencyWarning = require('./module-dependency-warning');
const env = require('../env')();
const shared = [new ModuleDependencyWarning()];
const client = [
new webpack.DefinePlugin(env.stringified),
new webpack.DefinePlugin({
__SERVER__: 'false',
__CLIENT__: 'true',
}),
new MiniCssExtractPlugin({
filename:
process.env.NODE_ENV === 'development' ? '[name].css' : '[name].[hash:8].css',
chunkFilename:
process.env.NODE_ENV === 'development'
? '[name].chunk.css'
: '[name].[chunkhash:8].chunk.css',
}),
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
new ManifestPlugin({
fileName: 'manifest.json',
writeToFileEmit: true, // fixes initial-only writing from WriteFileWebpackPlugin
}),
new StatsWriterPlugin({
fileName: 'stats.json',
fields: null,
transform(data) {
const trans = {};
trans.modules = data.modules.map(m => ({
id: m.id,
chunks: m.chunks,
reasons: m.reasons,
}));
trans.chunks = data.chunks.map(c => ({
id: c.id,
files: c.files,
}));
return JSON.stringify(trans, null, 2);
},
}),
];
const server = [
new webpack.DefinePlugin({
__SERVER__: 'true',
__CLIENT__: 'false',
}),
];
module.exports = {
shared,
client,
server,
};

View File

@ -0,0 +1,20 @@
const paths = require('../paths');
module.exports = {
extensions: ['.ts', '.tsx', '.js', '.mjs', '.json'],
modules: paths.resolveModules,
// tsconfig.compilerOptions.paths should sync with these
alias: {
contracts: paths.contractsBuild, // truffle build contracts dir
api: `${paths.srcClient}/api`,
components: `${paths.srcClient}/components`,
lib: `${paths.srcClient}/lib`,
modules: `${paths.srcClient}/modules`,
pages: `${paths.srcClient}/pages`,
store: `${paths.srcClient}/store`,
styles: `${paths.srcClient}/styles`,
typings: `${paths.srcClient}/typings`,
utils: `${paths.srcClient}/utils`,
web3interact: `${paths.srcClient}/web3interact`,
},
};

View File

@ -0,0 +1,36 @@
const path = require('path');
const nodeExternals = require('webpack-node-externals');
const paths = require('../paths');
const { server: serverLoaders } = require('./loaders');
const resolvers = require('./resolvers');
const plugins = require('./plugins');
module.exports = {
name: 'server',
target: 'node',
entry: {
server: [path.join(paths.srcServer, 'index.tsx')],
},
externals: [
nodeExternals({
// we still want imported css from external files to be bundled otherwise 3rd party packages
// which require us to include their own css would not work properly
whitelist: [/\.css$/, /^antd.*style$/],
}),
],
output: {
path: paths.serverBuild,
filename: 'server.js',
publicPath: paths.publicPath,
// libraryTarget: 'commonjs2',
},
resolve: { ...resolvers },
module: {
rules: serverLoaders,
},
plugins: [...plugins.shared, ...plugins.server],
stats: {
colors: true,
},
};

View File

@ -0,0 +1,19 @@
const baseConfig = require('./server.base');
const webpack = require('webpack');
const WriteFileWebpackPlugin = require('write-file-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const config = {
...baseConfig,
plugins: [
new WriteFileWebpackPlugin(),
...baseConfig.plugins,
new ForkTsCheckerWebpackPlugin(),
],
mode: 'development',
performance: {
hints: false,
},
};
module.exports = config;

View File

@ -0,0 +1,6 @@
const config = require('./server.base');
module.exports = {
...config,
mode: 'production',
};

0
frontend/logs/.gitkeep Normal file
View File

View File

@ -5,10 +5,10 @@
"license": "MIT",
"scripts": {
"analyze": "NODE_ENV=production ANALYZE=true next build ./client",
"build": "NODE_ENV=production next build ./client",
"dev": "rm -rf ./node_modules/.cache && cross-env BACKEND_URL=http://localhost:5000 node server.js",
"build": "cross-env NODE_ENV=production node bin/build.js",
"dev": "rm -rf ./node_modules/.cache && cross-env NODE_ENV=development BACKEND_URL=http://localhost:5000 node bin/dev.js",
"lint": "tslint --project ./tsconfig.json --config ./tslint.json -e \"**/build/**\"",
"start": "NODE_ENV=production node server.js",
"start": "NODE_ENV=production node build/server/server.js",
"tsc": "tsc",
"link-contracts": "cd client/lib && ln -s ../../build/contracts contracts",
"ganache": "ganache-cli -b 5",
@ -27,7 +27,25 @@
]
},
"dependencies": {
"@babel/core": "^7.0.1",
"@babel/plugin-proposal-class-properties": "^7.0.0",
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-transform-modules-commonjs": "^7.0.0",
"@babel/polyfill": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.0.0",
"@babel/register": "^7.0.0",
"@svgr/webpack": "^2.4.0",
"@types/classnames": "^2.2.6",
"@types/cors": "^2.8.4",
"@types/dotenv": "^4.0.3",
"@types/express": "^4.16.0",
"@types/express-winston": "^3.0.0",
"@types/headroom": "^0.7.31",
"@types/history": "^4.7.0",
"@types/i18next": "^8.4.5",
"@types/js-cookie": "2.1.0",
"@types/jwt-decode": "^2.2.1",
"@types/lodash": "^4.14.112",
@ -39,8 +57,17 @@
"@types/react": "16.3.16",
"@types/react-document-title": "^2.0.2",
"@types/react-dom": "^16.0.6",
"@types/react-helmet": "^5.0.7",
"@types/react-i18next": "^7.8.2",
"@types/react-redux": "^6.0.2",
"@types/react-router": "^4.0.31",
"@types/react-router-dom": "^4.3.1",
"@types/recompose": "^0.26.1",
"@types/redux-actions": "^2.3.0",
"@types/redux-logger": "^3.0.6",
"@types/webpack": "^4.4.11",
"@types/webpack-env": "^1.13.6",
"@types/winston": "^2.4.4",
"@zeit/next-css": "^0.2.0",
"@zeit/next-less": "^0.3.0",
"@zeit/next-sass": "^0.2.0",
@ -48,6 +75,10 @@
"ant-design-pro": "^2.0.0-beta.2",
"antd": "^3.7.1",
"axios": "^0.18.0",
"babel-core": "^6.26.3",
"babel-loader": "^8.0.2",
"babel-plugin-dynamic-import-node": "^2.1.0",
"babel-plugin-dynamic-import-webpack": "^1.0.2",
"babel-plugin-import": "^1.8.0",
"babel-plugin-inline-dotenv": "^1.1.2",
"babel-plugin-module-resolver": "^3.1.1",
@ -56,26 +87,37 @@
"babel-register": "^6.26.0",
"babel-runtime": "^6.26.0",
"bn.js": "4.11.8",
"body-parser": "^1.18.3",
"chalk": "^2.4.1",
"cookie-parser": "^1.4.3",
"cors": "^2.8.4",
"cross-env": "^5.2.0",
"dotenv": "6.0.0",
"css-loader": "^1.0.0",
"dotenv": "^6.0.0",
"drizzle": "^1.2.2",
"ethereum-blockies-base64": "1.0.2",
"ethereumjs-util": "5.2.0",
"express": "^4.16.3",
"file-loader": "^1.1.11",
"express-manifest-helpers": "^0.5.0",
"express-winston": "^3.0.0",
"file-loader": "^2.0.0",
"find-root": "^1.1.0",
"font-awesome": "^4.7.0",
"fork-ts-checker-webpack-plugin": "^0.4.2",
"fs-extra": "^7.0.0",
"http-proxy-middleware": "^0.18.0",
"husky": "^1.0.0-rc.8",
"i18next": "^11.9.0",
"is-mobile": "^1.0.0",
"isomorphic-unfetch": "^2.0.0",
"js-cookie": "^2.2.0",
"jwt-decode": "^2.2.0",
"less": "^3.7.1",
"lint-staged": "^7.1.3",
"less-loader": "^4.1.0",
"lint-staged": "^7.2.2",
"loadable-components": "^2.2.3",
"lodash": "^4.17.10",
"mini-css-extract-plugin": "^0.4.2",
"moment": "^2.22.2",
"next": "^6.1.1",
"next-compose-plugins": "^2.1.1",
@ -84,6 +126,7 @@
"next-redux-wrapper": "^2.0.0-beta.6",
"next-routes": "^1.4.2",
"node-sass": "^4.9.2",
"nodemon": "^1.18.4",
"nprogress": "^0.2.0",
"openzeppelin-solidity": "^1.12.0",
"prettier": "^1.13.4",
@ -92,11 +135,17 @@
"rc-scroll-anim": "^2.0.2",
"rc-tween-one": "^1.5.5",
"react": "^16.4.0",
"react-dev-utils": "^5.0.2",
"react-document-title": "^2.0.3",
"react-dom": "^16.4.0",
"react-headroom": "^2.2.2",
"react-helmet": "^5.2.0",
"react-hot-loader": "^4.3.8",
"react-i18next": "^7.12.0",
"react-mde": "^5.8.0",
"react-redux": "^5.0.7",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
"recompose": "^0.27.1",
"redux": "^4.0.0",
"redux-devtools-extension": "^2.13.2",
@ -107,17 +156,30 @@
"showdown": "^1.8.6",
"slate": "^0.37.3",
"slate-react": "^0.15.4",
"styled-components": "^3.3.3",
"stats-webpack-plugin": "^0.7.0",
"style-loader": "^0.23.0",
"styled-components": "^3.4.6",
"truffle-hdwallet-provider": "0.0.6",
"ts-loader": "^5.1.1",
"tslint": "^5.10.0",
"tslint-config-airbnb": "^5.9.2",
"tslint-config-prettier": "^1.13.0",
"tslint-eslint-rules": "^5.3.1",
"tslint-react": "^3.6.0",
"typescript": "3.0.3",
"url-loader": "^1.0.1",
"uglifyjs-webpack-plugin": "^2.0.0",
"url-loader": "^1.1.1",
"web3": "^1.0.0-beta.34",
"webpack-bundle-analyzer": "^2.13.1",
"webpack": "^4.19.0",
"webpack-bundle-analyzer": "^3.0.2",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.8",
"webpack-hot-middleware": "^2.24.0",
"webpack-manifest-plugin": "^2.0.4",
"webpack-node-externals": "^1.7.2",
"webpack-stats-plugin": "^0.2.1",
"winston": "^3.1.0",
"write-file-webpack-plugin": "^4.4.0",
"xss": "1.0.3"
},
"devDependencies": {

View File

@ -0,0 +1,61 @@
import * as React from 'react';
import Helmet from 'react-helmet';
export interface Props {
children: any;
css: string[];
scripts: string[];
state: string;
loadableStateScript: string;
styleElements: any;
}
export default class HTML extends React.Component<Props> {
render() {
const head = Helmet.renderStatic();
const {
children,
scripts,
css,
state,
loadableStateScript,
styleElements,
} = this.props;
return (
<html lang="">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
{/* TODO: import from @fortawesome */}
<link
rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.2.0/css/all.css"
integrity="sha384-hWVjflwFxL6sNzntih27bfxkr27PmbbK/iSvJ+a4+0owXq79v+lsFkW54bOGbiDQ"
crossOrigin="anonymous"
/>
{head.base.toComponent()}
{head.title.toComponent()}
{head.meta.toComponent()}
{head.link.toComponent()}
{head.script.toComponent()}
{css.map(href => {
return <link key={href} rel="stylesheet" href={href} />;
})}
{styleElements.map((styleEl: any) => styleEl)}
<script
dangerouslySetInnerHTML={{
__html: `window.__PRELOADED_STATE__ = ${state}`,
}}
/>
</head>
<body>
<div id="app" dangerouslySetInnerHTML={{ __html: children }} />
<script dangerouslySetInnerHTML={{ __html: loadableStateScript }} />
{scripts.map(src => {
return <script key={src} src={src} />;
})}
</body>
</html>
);
}
}

71
frontend/server/index.tsx Normal file
View File

@ -0,0 +1,71 @@
import express from 'express';
import * as cors from 'cors';
import * as path from 'path';
import chalk from 'chalk';
import manifestHelpers from 'express-manifest-helpers';
import * as bodyParser from 'body-parser';
import dotenv from 'dotenv';
import expressWinston from 'express-winston';
import log from './log';
import serverRender from './render';
// @ts-ignore
import * as paths from '../config/paths';
const isDev = process.env.NODE_ENV === 'development';
dotenv.config();
const app = express();
// log requests
app.use(expressWinston.logger({ winstonInstance: log }));
if (isDev) {
app.use(
paths.publicPath,
express.static(path.join(paths.clientBuild, paths.publicPath)),
);
// tslint:disable-next-line:variable-name
app.use('/favicon.ico', (_req, res) => {
res.send('');
});
} else {
log.warn('PRODUCTION mode, serving static assets from node server.');
app.use(
paths.publicPath,
express.static(path.join(paths.clientBuild, paths.publicPath)),
);
// tslint:disable-next-line:variable-name
app.use('/favicon.ico', (_req, res) => {
res.send('');
});
}
app.use(cors());
app.use(bodyParser.json());
const manifestPath = path.join(paths.clientBuild, paths.publicPath);
app.use(
manifestHelpers({
manifestPath: `${manifestPath}/manifest.json`,
cache: process.env.NODE_ENV === 'production',
// prependPath: '//cdn.example/assets' // if statics are elsewhere
}),
);
app.use(serverRender());
app.use(expressWinston.errorLogger({ winstonInstance: log }));
app.listen(process.env.PORT || 3000, () => {
const port = process.env.PORT || 3000;
if (isDev) {
console.log(chalk.blue(`App is running: 🌎 http://localhost:${port} `));
} else {
log.info(`Server started on port ${port}`);
}
});
export default app;

55
frontend/server/log.ts Normal file
View File

@ -0,0 +1,55 @@
import { createLogger, format, transports } from 'winston';
// @ts-ignore
import * as paths from '../config/paths';
const { combine, timestamp, prettyPrint, printf, colorize } = format;
const custom = combine(
timestamp({ format: 'YY/MM/DD HH:mm:ss' }),
colorize(),
printf(info => `${info.timestamp}[${info.level}] ${info.message}`),
);
const enumerateErrorFormat = format((info: any) => {
if (info.message instanceof Error) {
info.message = Object.assign(
{
message: info.message.message,
stack: info.message.stack,
},
info.message,
);
}
if (info instanceof Error) {
return Object.assign(
{
message: info.message,
stack: info.stack,
},
info,
);
}
return info;
});
// levels: error, warn, info, verbose, debug, silly
const log = createLogger({
level: 'verbose',
exitOnError: true,
format: combine(enumerateErrorFormat(), timestamp(), prettyPrint()),
transports: [
new transports.File({
filename: `${paths.logs}/app.log`,
level: 'info',
handleExceptions: true,
maxsize: 5242880, // 5MB
maxFiles: 5,
}),
new transports.Console({
level: 'verbose',
handleExceptions: true,
format: custom,
}),
],
});
export default log;

141
frontend/server/render.tsx Normal file
View File

@ -0,0 +1,141 @@
import React from 'react';
import { Request, Response } from 'express';
import { renderToString } from 'react-dom/server';
import { ServerStyleSheet } from 'styled-components';
import { getLoadableState } from 'loadable-components/server';
import { StaticRouter as Router } from 'react-router-dom';
import { Provider } from 'react-redux';
// import IntlProvider from '../shared/i18n/IntlProvider';
import log from './log';
import { configureStore } from '../client/store/configure';
// import DrizzleContext from '../shared/DrizzleContext';
// import App from '../shared/App';
import Html from './components/HTML';
import Routes from '../client/Routes';
import fs from 'fs';
import path from 'path';
// @ts-ignore
import * as paths from '../config/paths';
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;
};
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: any[], m: any) => a.concat(m.chunks), []);
const files = stats.chunks
.filter((c: any) => chunks.indexOf(c.id) > -1)
.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)),
};
});
// react-router recommends agains redux - router integration, perhaps remove?
// https://reacttraining.com/react-router/web/guides/redux-integration
// <DrizzleContext.Provider store={store}>
// <IntlProvider>
// <App />
// </IntlProvider>
// </DrizzleContext.Provider>
const serverRenderer = () => async (req: Request, res: Response) => {
// const store = configureStore(req.url);
const store = configureStore();
const sheet = new ServerStyleSheet();
const reactApp = (
<Provider store={store}>
<Router location={req.url} context={{}}>
<Routes />
</Router>
</Provider>
);
let loadableState;
let loadableFiles;
// 1. loadable state will render dynamic imports
try {
loadableState = await getLoadableState(reactApp);
loadableFiles = await chunkExtractFromLoadables(loadableState);
} 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. styled components will gather styles & wrap in provider
const styleConnectedApp = sheet.collectStyles(reactApp);
const styleElements = sheet.getStyleElement();
const content = renderToString(styleConnectedApp);
const state = JSON.stringify(store.getState());
// ! ensure manifest.json is available
try {
res.locals.getManifest();
} catch (e) {
const disp =
'ERROR: Could not load client manifest.json, there was probably a client build error.';
log.error(disp);
return res.status(500).send(disp);
}
const cssFiles = ['bundle.css', 'vendor.css', ...loadableFiles.css]
.map(f => res.locals.assetPath(f))
.filter(Boolean);
const jsFiles = [...loadableFiles.js, 'vendor.js', 'bundle.js']
.map(f => res.locals.assetPath(f))
.filter(Boolean);
return res.send(
'<!doctype html>' +
renderToString(
<Html
css={cssFiles}
styleElements={styleElements}
scripts={jsFiles}
state={state}
loadableStateScript={loadableState.getScriptContent()}
>
{content}
</Html>,
),
);
};
export default serverRenderer;

View File

@ -15,9 +15,22 @@
"preserveConstEnums": true,
"sourceMap": true,
"skipLibCheck": true,
"baseUrl": "./client",
"lib": ["dom", "es2017"]
"baseUrl": ".",
"lib": ["dom", "es2017"],
"paths": {
"contracts/*": ["../contract/build/contracts/*"],
"api/*": ["./client/api/*"],
"components/*": ["./client/components/*"],
"lib/*": ["./client/lib/*"],
"modules/*": ["./client/modules/*"],
"pages/*": ["./client/pages/*"],
"store/*": ["./client/store/*"],
"styles/*": ["./client/styles/*"],
"typings/*": ["./client/typings/*"],
"utils/*": ["./client/utils/*"],
"web3interact/*": ["./client/web3interact/*"]
}
},
"include": ["./client", "node_modules/@types", "typings"],
"exclude": ["./client/build", "./client/static"]
"include": ["./client/**/*", "./server/**/*"],
"exclude": ["./client/static"]
}

View File

@ -9,5 +9,8 @@
"interface-name": [true, "never-prefix"],
"curly": [true, "ignore-same-line"],
"no-console": false
},
"linterOptions": {
"exclude": ["client/lib/contracts/**/*.json"]
}
}

File diff suppressed because it is too large Load Diff