├── src
├── client
│ ├── styles
│ │ └── index.scss
│ ├── components
│ │ ├── home.tsx
│ │ ├── notFound.tsx
│ │ └── items.tsx
│ ├── model
│ │ └── middleware.ts
│ ├── router.ts
│ ├── reducers
│ │ └── index.ts
│ ├── root.tsx
│ ├── store
│ │ └── index.ts
│ └── index.tsx
└── server
│ ├── index.ts
│ ├── wds.ts
│ └── ssr
│ ├── component.tsx
│ ├── html.tsx
│ └── index.ts
├── .editorconfig
├── .gitignore
├── webpack
├── vendor.js
├── common.js
├── plugins
│ └── assetsManifest.js
├── config.development.js
├── config.common.js
└── config.production.js
├── docker-compose.yml
├── nodemon.json
├── tslint.json
├── tsconfig.json
├── typings
└── global.d.ts
├── config
└── index.js
├── webpack.config.js
├── README.md
└── package.json
/src/client/styles/index.scss:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #eee;
3 | }
4 |
5 | .any {
6 | display: flex;
7 | }
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | indent_style = space
3 | end_of_line = lf
4 | indent_size = 2
5 | charset = utf-8
6 | trim_trailing_whitespace = true
7 |
8 | [*.md]
9 | max_line_length = 0
10 | trim_trailing_whitespace = false
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Project excludes
2 |
3 | # Build folder
4 | dist
5 | public
6 |
7 | # Dependencies folder
8 | node_modules
9 | ts-node-*
10 |
11 | # Misc excludes
12 | .idea
13 | *.log
14 | *.swp
15 | *.map
16 | .v8flags.*
17 |
--------------------------------------------------------------------------------
/src/client/components/home.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Helmet from 'react-helmet';
3 |
4 | export default class Home extends React.Component {
5 | public render () {
6 | return (
7 |
8 |
9 | Home
10 |
11 | );
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/webpack/vendor.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const VENDOR_LIBS = [
4 | 'react',
5 | 'react-dom',
6 | 'react-helmet',
7 | 'react-redux',
8 | 'react-router',
9 | 'react-router-config',
10 | 'react-router-dom',
11 | 'redux',
12 |
13 | 'promise',
14 | 'lodash',
15 | 'isomorphic-fetch'
16 | ];
17 |
18 | module.exports = VENDOR_LIBS;
19 |
--------------------------------------------------------------------------------
/src/client/model/middleware.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch } from 'redux';
2 |
3 | export function asyncMiddleware (extraArgument?: any) {
4 | return ({ dispatch, getState }) => (next) => (action) => {
5 | if (typeof action === 'function') {
6 | return action(dispatch, getState, extraArgument);
7 | }
8 | return next(action);
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 |
3 | services:
4 | app:
5 | image: node:8.6
6 | container_name: typescript-react-express
7 | volumes:
8 | - .:/app
9 | entrypoint:
10 | - /bin/bash
11 | ports:
12 | - 3000:3000
13 | - 5858:5858
14 | - 9229:9229
15 | command: -s
16 | working_dir: /app
17 | tty: true
18 |
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "delay": "2000",
3 | "verbose": true,
4 | "ignore": [
5 | "dist",
6 | "public"
7 | ],
8 | "watch": [
9 | "src/server/**/*.{ts,tsx}",
10 | "config",
11 | "webpack"
12 | ],
13 | "env": {
14 | "NODE_ENV": "development"
15 | },
16 | "ext": "js json ts tsx",
17 | "exec": "ts-node --inspect src/server/index.ts"
18 | }
19 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [ "tslint:latest", "tslint-react" ],
3 | "rules": {
4 | "quotemark": [ true, "single" ],
5 | "space-before-function-paren": true,
6 | "ordered-imports": false,
7 | "trailing-comma": false,
8 | "no-var-requires": false,
9 | "no-console": false,
10 | "no-implicit-dependencies": false,
11 | "no-submodule-imports": false
12 | }
13 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es5",
5 | "jsx": "react",
6 | "alwaysStrict": true,
7 | "sourceMap": true,
8 | "outDir": "dist",
9 | "lib": [
10 | "dom",
11 | "es2015",
12 | "es5",
13 | "es6"
14 | ]
15 | },
16 | "include": [
17 | "src/**/*.ts",
18 | "src/**/*.tsx",
19 | "typings"
20 | ],
21 | "exclude": [
22 | "node_modules"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/typings/global.d.ts:
--------------------------------------------------------------------------------
1 |
2 | interface NodeModule {
3 | hot: {
4 | accept: Function;
5 | };
6 | }
7 |
8 | interface IGlobalVar {
9 | __CLIENT__: boolean;
10 | __SERVER__: boolean;
11 | __DEV__: boolean;
12 | __TEST__: boolean;
13 | }
14 |
15 | declare namespace NodeJS {
16 | interface Global extends IGlobalVar { }
17 | }
18 |
19 | interface Window extends IGlobalVar {
20 | __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any;
21 | __PRELOADED_STATE__: any;
22 | }
23 |
--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 |
5 | function config (environment = 'production') {
6 | const SRC_FOLDER = path.resolve(__dirname, '..', 'src');
7 |
8 | return {
9 | PORT: parseInt(process.env.PORT, 10) || 3000,
10 | SRC_FOLDER: SRC_FOLDER,
11 | PUBLIC_PATH: '/static/',
12 | PUBLIC_FOLDER: path.resolve(__dirname, '..', 'public'),
13 | SRC_CLIENT_FOLDER: path.join(SRC_FOLDER, 'client')
14 | };
15 | }
16 |
17 | module.exports = config;
18 |
--------------------------------------------------------------------------------
/src/client/router.ts:
--------------------------------------------------------------------------------
1 | import { RouteConfig } from 'react-router-config';
2 |
3 | import Root from './root';
4 | import Items from './components/items';
5 | import NotFound from './components/notFound';
6 | import Home from './components/home';
7 |
8 | export const routes: RouteConfig[] = [
9 | {
10 | component: Root,
11 | routes: [
12 | {
13 | path: '/',
14 | exact: true,
15 | component: Home
16 | },
17 | {
18 | path: '/items',
19 | component: Items
20 | },
21 | {
22 | path: '*',
23 | component: NotFound
24 | }
25 | ]
26 | }
27 | ];
28 |
--------------------------------------------------------------------------------
/src/client/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import * as _ from 'lodash';
2 | import { ActionCreator } from 'redux';
3 |
4 | const USERS_LOADED = '@ssr/users/loaded';
5 | const initialState = {
6 | items: []
7 | };
8 |
9 | export function reducer (state = initialState, action) {
10 | switch (action.type) {
11 | case USERS_LOADED:
12 | return _.assign({}, state, { items: action.items });
13 |
14 | default:
15 | return state;
16 | }
17 | }
18 |
19 | export const fetchUsers: any = () => (dispatch) => {
20 | return fetch('//jsonplaceholder.typicode.com/users')
21 | .then((res) => res.json())
22 | .then((users) => {
23 | dispatch({
24 | type: USERS_LOADED,
25 | items: users
26 | });
27 | });
28 | };
29 |
--------------------------------------------------------------------------------
/src/client/components/notFound.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Route } from 'react-router-dom';
3 | import Helmet from 'react-helmet';
4 |
5 | function NotFound ({ staticContext }) {
6 | // will be available only on the server
7 | if (staticContext) {
8 | staticContext.status = 404;
9 | }
10 | const title = 'Page Not Found';
11 | const meta = [
12 | { name: 'description', content: 'A page to say hello asynchronously' },
13 | ];
14 |
15 | return (
16 |
17 |
18 |
404 : Not Found
19 |
20 | );
21 | }
22 |
23 | export function NotFoundRoute () {
24 | return (
25 |
26 | );
27 | }
28 |
29 | export default NotFoundRoute;
30 |
--------------------------------------------------------------------------------
/webpack/common.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const isDevelopment = (process.env.NODE_ENV || 'development') === 'development';
4 | const autoprefixer = require('autoprefixer');
5 |
6 | module.exports = {
7 | scssLoader: [
8 | {
9 | loader: 'css-loader',
10 | options: {
11 | minimize: !isDevelopment,
12 | sourceMap: isDevelopment
13 | }
14 | },
15 | {
16 | loader: 'postcss-loader',
17 | options: {
18 | sourceMap: isDevelopment,
19 | plugins: [
20 | autoprefixer({
21 | browsers:['ie >= 8', 'last 4 version']
22 | })
23 | ]
24 | }
25 | },
26 | {
27 | loader: 'sass-loader',
28 | options: {
29 | sourceMap: isDevelopment
30 | }
31 | }
32 | ]
33 | };
34 |
--------------------------------------------------------------------------------
/webpack/plugins/assetsManifest.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 |
4 | class AssetsManifest {
5 | constructor(options) {
6 | this.options = options || {};
7 | }
8 |
9 | apply(compiler) {
10 | compiler.plugin('done', function(stats) {
11 | const assets = stats.toJson().assetsByChunkName;
12 | const pathFile = path.join(compiler.options.output.path, 'manifest.json');
13 |
14 | fs.stat(compiler.options.output.path, (err, stats) => {
15 | // TODO: Make better
16 | if (err) {
17 | fs.mkdir(compiler.options.output.path, (err) => {
18 | fs.writeFileSync(pathFile, JSON.stringify(assets));
19 | });
20 | } else {
21 | fs.writeFileSync(pathFile, JSON.stringify(assets));
22 | }
23 | });
24 | });
25 | }
26 | }
27 |
28 | module.exports = AssetsManifest;
29 |
--------------------------------------------------------------------------------
/src/client/root.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Helmet from 'react-helmet';
3 |
4 | import { Redirect, Link, Route } from 'react-router-dom';
5 | import itemsComponent from './components/items';
6 | import { renderRoutes, RouteConfig } from 'react-router-config';
7 |
8 | interface IRootProps {
9 | route: {
10 | routes: RouteConfig[];
11 | };
12 | }
13 |
14 | export default class Root extends React.Component {
15 | public render () {
16 | const APP_NAME = 'any app name';
17 | return (
18 |
19 |
20 |
Home
21 |
items
22 |
Not Found
23 |
24 | {renderRoutes(this.props.route.routes)}
25 |
26 | );
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/server/index.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 | import wds from './wds';
3 | import ssr from './ssr';
4 |
5 | if (typeof process.env.NODE_ENV === 'undefined') {
6 | process.env.NODE_ENV = 'production';
7 | }
8 |
9 | const isDevelopment = process.env.NODE_ENV === 'development';
10 |
11 | global.__CLIENT__ = false;
12 | global.__SERVER__ = true;
13 | global.__DEV__ = isDevelopment;
14 | global.__TEST__ = false;
15 |
16 | const config = require('../../config')(process.env.NODE_ENV);
17 | const app = express();
18 |
19 | if (isDevelopment) {
20 | wds(app);
21 | } else {
22 | app.use(config.PUBLIC_PATH, express.static(config.PUBLIC_FOLDER));
23 | }
24 |
25 | app.get('*', ssr);
26 |
27 | app.listen(config.PORT, (err) => {
28 | if (err) {
29 | throw err;
30 | }
31 |
32 | console.log('===> Starting Server . . .');
33 | console.log('===> Port: ' + config.PORT);
34 | console.log('===> Environment: ' + process.env.NODE_ENV, ', isDevelopment', isDevelopment);
35 | });
36 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const _ = require('lodash');
4 |
5 | function mergeWebpackConfigHelper(objValue, srcValue) {
6 | if (_.isArray(objValue)) {
7 | return objValue.concat(srcValue);
8 | }
9 | }
10 |
11 | const configList = {
12 | development: require('./webpack/config.development'),
13 | common: require('./webpack/config.common'),
14 | production: require('./webpack/config.production')
15 | }
16 |
17 | /**
18 | * @param {string} env
19 | * @return {object}
20 | */
21 | function getConfig (env) {
22 | if (_.isUndefined(env)) {
23 | throw new Error('Can\'t find local environment variable via process.env.NODE_ENV');
24 | }
25 |
26 | if (_.isUndefined(configList[env]) || env === 'common') {
27 | throw new Error('Can\'t find environments see configList object');
28 | }
29 |
30 | return _.mergeWith(
31 | {},
32 | configList[env](__dirname),
33 | configList.common(__dirname),
34 | mergeWebpackConfigHelper
35 | );
36 | }
37 |
38 | module.exports = getConfig(process.env.NODE_ENV);
39 |
--------------------------------------------------------------------------------
/src/client/store/index.ts:
--------------------------------------------------------------------------------
1 | import { compose, createStore, applyMiddleware, Store, Middleware } from 'redux';
2 |
3 | import { reducer } from '../reducers';
4 | import { asyncMiddleware } from '../model/middleware';
5 |
6 | export function configureStore (initStore: {} = {}) {
7 | let composeEnhancers = compose;
8 | const enhancers: any[] = [];
9 | const middleware: Middleware[] = [
10 | asyncMiddleware()
11 | ];
12 |
13 | if (typeof window !== 'undefined' && typeof window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ === 'function') {
14 | composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
15 | }
16 |
17 | const store = createStore(
18 | reducer,
19 | initStore,
20 | composeEnhancers(
21 | applyMiddleware(...middleware),
22 | ...enhancers
23 | )
24 | );
25 |
26 | if (module.hot) {
27 | // Enable Webpack hot module replacement for reducers
28 | module.hot.accept('../reducers', () => {
29 | const nextRootReducer = require('../reducers');
30 | store.replaceReducer(nextRootReducer);
31 | });
32 | }
33 |
34 | return store;
35 | }
36 |
--------------------------------------------------------------------------------
/webpack/config.development.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const webpack = require("webpack");
5 |
6 | const config = require('../config')(process.env.NODE_ENV);
7 | const common = require('./common');
8 |
9 | module.exports = function getDevelopmentConfig (dirname) {
10 | return {
11 | devtool: 'inline-source-map',
12 | entry: {
13 | app: [
14 | "react-hot-loader/patch",
15 | path.join(config.SRC_CLIENT_FOLDER, 'index'),
16 | 'webpack-hot-middleware/client',
17 | 'webpack/hot/dev-server'
18 | ]
19 | },
20 | module: {
21 | loaders: [
22 | {
23 | test: /\.tsx?$/,
24 | use: [ 'react-hot-loader/webpack', 'awesome-typescript-loader' ],
25 | include: config.SRC_CLIENT_FOLDER,
26 | exclude: path.resolve(dirname, 'node_modules')
27 | },
28 | {
29 | test: /\.scss$/,
30 | use: [ "style-loader", ...common.scssLoader ]
31 | }
32 | ]
33 | },
34 | plugins: [
35 | new webpack.HotModuleReplacementPlugin(),
36 | new webpack.NamedModulesPlugin()
37 | ]
38 | };
39 | };
40 |
--------------------------------------------------------------------------------
/src/client/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import { BrowserRouter } from 'react-router-dom';
5 | import { renderRoutes, RouteConfig } from 'react-router-config';
6 | import { AppContainer } from 'react-hot-loader';
7 |
8 | import 'isomorphic-fetch';
9 | import './styles/index.scss';
10 |
11 | import { routes } from './router';
12 | import { configureStore } from './store';
13 |
14 | const element = document.getElementById('root');
15 | const preloadedState = window.__PRELOADED_STATE__;
16 | const store = configureStore(preloadedState);
17 |
18 | delete window.__PRELOADED_STATE__;
19 |
20 | function render (route: RouteConfig[]) {
21 | const childContent = (
22 |
23 |
24 |
25 | {renderRoutes(route)}
26 |
27 |
28 |
29 | );
30 |
31 | ReactDOM.hydrate(childContent, element);
32 | }
33 |
34 | render(routes);
35 |
36 | // Hot Module Replacement API
37 | if (module.hot) {
38 | module.hot.accept('./router', () => {
39 | const newRoutes = require('./router').routes;
40 | render(newRoutes);
41 | });
42 | }
43 |
--------------------------------------------------------------------------------
/webpack/config.common.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const webpack = require("webpack");
5 |
6 | const config = require('../config')(process.env.NODE_ENV);
7 | const vendors = require('./vendor');
8 |
9 | const NODE_ENV = process.env.NODE_ENV || 'development';
10 |
11 | module.exports = function getConfig(dirname) {
12 | return {
13 | target: 'web',
14 | context: path.resolve(dirname),
15 | entry: {
16 | vendor: vendors
17 | },
18 | resolve: {
19 | extensions: [ '.ts', '.tsx', '.js', '.scss', '.css' ]
20 | },
21 | output: {
22 | path: config.PUBLIC_FOLDER,
23 | filename: '[name].[hash].js',
24 | chunkFilename: '[name].[chunkhash].js',
25 | publicPath: config.PUBLIC_PATH
26 | },
27 | module: {
28 | loaders: []
29 | },
30 | plugins: [
31 | new webpack.NoEmitOnErrorsPlugin(),
32 | new webpack.optimize.CommonsChunkPlugin({
33 | name: 'vendor'
34 | }),
35 | new webpack.DefinePlugin({
36 | 'process.env': {
37 | NODE_ENV: JSON.stringify(NODE_ENV)
38 | },
39 | __CLIENT__: true,
40 | __SERVER__: false,
41 | __DEV__: NODE_ENV === 'development',
42 | __TEST__: false
43 | })
44 | ]
45 | };
46 | };
47 |
--------------------------------------------------------------------------------
/src/client/components/items.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Store, bindActionCreators} from 'redux';
3 | import * as ReactRedux from 'react-redux';
4 | import * as reactRouter from 'react-router';
5 | import Helmet from 'react-helmet';
6 |
7 | import { fetchUsers } from '../reducers';
8 |
9 | class Items extends React.Component {
10 | public static fetchData (store: Store, match: reactRouter.match) {
11 | return store.dispatch(fetchUsers());
12 | }
13 |
14 | public constructor () {
15 | super();
16 | this.renderItems = this.renderItems.bind(this);
17 | }
18 |
19 | public componentDidMount () {
20 | this.props.fetchUsers();
21 | }
22 |
23 | public render () {
24 | return (
25 |
26 |
27 | {this.props.items.map(this.renderItems)}
28 |
29 | );
30 | }
31 |
32 | private renderItems (item) {
33 | return (
34 |
35 | {item.name}
36 |
37 | );
38 | }
39 | }
40 |
41 | const mapDispatchToProps = (dispatch) => bindActionCreators({ fetchUsers }, dispatch);
42 | const mapStateToProps = (state) => ({items: state.items});
43 |
44 | export default ReactRedux.connect(mapStateToProps, mapDispatchToProps)(Items);
45 |
--------------------------------------------------------------------------------
/src/server/wds.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | /**
4 | * webpack dev server
5 | * @param {express.Application} app express app
6 | */
7 | export function wds (app: express.Application) {
8 | const webpackConfig = require('../../webpack.config');
9 | const webpackDevMiddleware = require('webpack-dev-middleware');
10 | const webpackHotMiddleware = require('webpack-hot-middleware');
11 | const webpack = require('webpack');
12 |
13 | const compiler = webpack(webpackConfig);
14 |
15 | app.use(webpackDevMiddleware(compiler, {
16 | hot: true,
17 | noInfo: true,
18 | publicPath: webpackConfig.output.publicPath,
19 | serverSideRender: true,
20 | // https://github.com/webpack/webpack-dev-server/issues/143#issuecomment-139705511
21 | watchOptions: {
22 | poll: true
23 | }
24 | }));
25 | app.use(webpackHotMiddleware(compiler));
26 |
27 | // Throw away the cached client modules and let them be re-required next time
28 | compiler.plugin('done', () => {
29 | const cacheModules = Object.keys(require.cache)
30 | .filter((id) => /client/.test(id) || /ssr/.test(id));
31 |
32 | if (cacheModules.length > 1) {
33 | console.info('===> Client\'s cache has been removed.', `Find ${cacheModules.length}`);
34 | cacheModules.forEach((id) => delete require.cache[id]);
35 | }
36 | });
37 | }
38 |
39 | export default wds;
40 |
--------------------------------------------------------------------------------
/src/server/ssr/component.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as _ from 'lodash';
3 | import { Store } from 'redux';
4 | import { Provider } from 'react-redux';
5 | import { StaticRouter } from 'react-router';
6 | import { renderRoutes, matchRoutes } from 'react-router-config';
7 |
8 | import { routes } from '../../client/router';
9 | import { configureStore } from '../../client/store';
10 |
11 | export interface IComponentConfig {
12 | store: Store;
13 | routerContext: {
14 | url?: string;
15 | action?: string;
16 | location?: any;
17 | status?: number;
18 | };
19 | locationUrl: string;
20 | }
21 |
22 | interface IComponentProps {
23 | config: IComponentConfig;
24 | }
25 |
26 | export function Component ({ config }: IComponentProps) {
27 | return (
28 |
29 |
33 | {renderRoutes(routes)}
34 |
35 |
36 | );
37 | }
38 |
39 | export function getStore (): Store {
40 | return configureStore();
41 | }
42 |
43 | export function fetchData (url: string, store: Store) {
44 | const branch = matchRoutes(routes, url);
45 |
46 | const promises = branch.map(({ route, match }) => {
47 | const fetchDataPromise = _.get(route, 'component.fetchData');
48 |
49 | return fetchDataPromise instanceof Function
50 | ? fetchDataPromise(store, match)
51 | : Promise.resolve(null);
52 | });
53 |
54 | return promises;
55 | }
56 |
--------------------------------------------------------------------------------
/webpack/config.production.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const webpack = require("webpack");
5 | const ExtractTextPlugin = require("extract-text-webpack-plugin");
6 |
7 | const common = require('./common');
8 | const config = require('../config')(process.env.NODE_ENV);
9 | const AssetsManifest = require('./plugins/assetsManifest');
10 |
11 | const extractSass = new ExtractTextPlugin({
12 | filename: "[name].[hash].css",
13 | });
14 |
15 | module.exports = function getProductionConfig (dirname) {
16 | return {
17 | entry: {
18 | app: path.join(config.SRC_CLIENT_FOLDER, 'index')
19 | },
20 | module: {
21 | loaders: [
22 | {
23 | test: /\.tsx?$/,
24 | use: [ 'awesome-typescript-loader' ],
25 | include: config.SRC_CLIENT_FOLDER,
26 | exclude: path.resolve(dirname, 'node_modules')
27 | },
28 | {
29 | test: /\.scss$/,
30 | use: extractSass.extract({
31 | use: common.scssLoader
32 | }),
33 | include: config.SRC_CLIENT_FOLDER,
34 | exclude: path.resolve(dirname, 'node_modules')
35 | }
36 | ]
37 | },
38 | plugins: [
39 | new AssetsManifest(),
40 | extractSass,
41 | new webpack.optimize.OccurrenceOrderPlugin(),
42 | new webpack.optimize.UglifyJsPlugin({
43 | beautify: false,
44 | comments: false,
45 | compress: {
46 | sequences: true,
47 | booleans: true,
48 | loops: true,
49 | unused: true,
50 | warnings: false,
51 | drop_console: true,
52 | unsafe: true
53 | }
54 | })
55 | ]
56 | };
57 | };
58 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Typescript React Redux Isomorphic Hot Example.
2 |
3 | ## About
4 | This is a starter boilerplate app I've put together using the following technologies:
5 |
6 | - React
7 | - React Router
8 | - Express
9 | - Typescript
10 | - Webpack for bundling
11 | - Webpack Dev Middleware
12 | - Webpack Hot Middleware
13 | - Redux's futuristic Flux implementation
14 | - Support ReduxDevTools (developer experience)
15 | - TSLint to maintain a consistent code style
16 | - style-loader, sass-loader and autoprefixer to allow import of stylesheets in plain css, scss,
17 | - react-helmet to manage title and meta tag information on both server and client
18 |
19 | ## Feature:
20 | - Support docker
21 | - Server side render + fetch data
22 | - React hot reload
23 | - Server render supports react hot reload
24 | - Server does not reload after change client code
25 |
26 | ## How to use
27 |
28 | ```sh
29 | $ npm run docker:run
30 |
31 | # Connect to docker container
32 | $ npm run docker:exec
33 |
34 | # NOTE. After work you should stop container
35 | $ npm run docker:stop
36 | ```
37 |
38 | ```sh
39 | # Developer mode
40 | $ npm run start
41 |
42 | $ npm run build
43 | $ npm run server
44 | ```
45 |
46 | ```sh
47 | # TSLint
48 | $ npm run tslint
49 |
50 | # Unit test
51 | # TODO
52 | ```
53 |
54 | ## Explanation
55 |
56 | ### Client side
57 | The client side entry point is reasonably named client/index.ts. All it does is load the routes, initiate react-router, rehydrate the redux state from the `window.__PRELOADED_STATE__` passed in from the server, and render the page over top of the server-rendered DOM. This makes React enable all its event listeners without having to re-render the DOM.
58 |
59 | ### Server-side Data Fetching
60 | ...
61 |
62 | ### Routing and HTML return
63 | ...
64 |
65 | ## TODO:
66 | - Storybook
67 | - test (mocha, chai, chai-http, enzyme, sinon, jsdom)
68 | - webpack/plugins/assetsManifest.js
69 | - Dockerfile
70 |
--------------------------------------------------------------------------------
/src/server/ssr/html.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as Redux from 'redux';
3 | import * as _ from 'lodash';
4 | import Helmet from 'react-helmet';
5 |
6 | interface IProps {
7 | content?: string;
8 | store: Redux.Store;
9 | assets?: any;
10 | publicPath?: string;
11 | }
12 |
13 | export class Html extends React.Component {
14 | public static get defaultProps (): Partial {
15 | return {
16 | content: '',
17 | publicPath: '/',
18 | assets: {},
19 | };
20 | }
21 |
22 | public render () {
23 | const __PRELOADED_STATE__ = JSON.stringify(this.props.store.getState()).replace(/
28 |
29 | {head.title.toComponent()}
30 | {head.meta.toComponent()}
31 |
32 |
33 | {this.renderCSS(this.props.assets.app)}
34 |
35 |
36 |
37 |
41 | {this.renderJS(this.props.assets.vendor)}
42 | {this.renderJS(this.props.assets.app)}
43 |
44 |