├── 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 |