├── .babelrc ├── .gitignore ├── .travis.yml ├── README.md ├── index.html ├── index.prod.html ├── karma.conf.js ├── module.d.ts ├── package-lock.json ├── package.json ├── postcss.config.js ├── require.d.ts ├── src ├── App.css ├── App.less ├── App.scss ├── App.tsx ├── Config.less ├── Counter.tsx ├── DevTools.tsx ├── Layout.tsx ├── LazyComponent.less ├── LazyComponent.tsx ├── Menu.tsx ├── Polyfill.tsx ├── PreloadedComponent.tsx ├── StyleguidistWrapper.tsx ├── calculator │ └── Calculator.ts ├── components │ └── demo │ │ ├── Demo.less │ │ ├── Demo.md │ │ └── Demo.tsx ├── config.ts ├── context │ └── Moo.tsx ├── history.ts ├── i18n.ts ├── index.tsx ├── reducers.ts ├── server.tsx ├── store.ts └── types │ └── globals.d.ts ├── styleguide.config.js ├── test ├── Calculator.test.ts ├── Counter.test.tsx └── main.js ├── tsconfig.json ├── tslint.json ├── typings └── redux.d.ts ├── webpack.config.common.js ├── webpack.config.dev.babel.js ├── webpack.config.dev.js ├── webpack.config.js ├── webpack.config.prod.js ├── webpack.config.server.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react"], 3 | "plugins": ["react-hot-loader/babel", "transform-class-properties", "dynamic-import-node"] 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | TESTS-* 3 | yarn-error.log 4 | dist 5 | /server -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Universal TypeScript + React 2 | ===================== 3 | Your React + TypeScript Swiss Army knife 4 | 5 | [![Build Status](https://travis-ci.org/zdanowiczkonrad/universal-typescript-react.svg?branch=master)](https://travis-ci.org/zdanowiczkonrad/universal-typescript-react) [![npm version](https://badge.fury.io/js/universal-typescript-react.svg)](https://badge.fury.io/js/universal-typescript-react) [![Dependencies](https://david-dm.org/zdanowiczkonrad/universal-typescript-react.svg)](https://david-dm.org/zdanowiczkonrad/universal-typescript-react.svg) 6 | 7 | Universal and up to date configuration with TypeScript and React and all modern frontend essentials, 8 | including dynamic imports, server-side rendering with prehydration and full hot module reload. 9 | Not forgetting about styles and all you need to develop webapps. Typesafely! 🙃 10 | 11 | **Heavily inspiried/influenced by:** 12 | https://github.com/gaearon/react-hot-boilerplate/ 13 | https://github.com/piotrwitek/react-redux-typescript-guide 14 | 15 | Quick checklist: 16 | * [x] Node 8 17 | * [x] Yarn 1.5 18 | * [x] Webpack 4 19 | * [x] TypeScript 2 (+ ts-loader) 20 | * [x] TypeScript import alias 21 | * [x] React 16+ 22 | * [x] Sourcemaps (prod + dev + tests) 23 | * [x] HMR (with local state keeping) 24 | * [x] fork-ts-checker (async compile) 25 | * [x] tslint 26 | * [x] Promise polyfill 27 | * [x] Tests (Karma + Jasmine) 28 | * [x] Enzyme (for React) 29 | * [x] Splitted dev/prod configs 30 | * [x] Vendor bundle/chunks 31 | * [x] Styles support (LESS/SASS/PostCSS) 32 | * [x] One CSS file per bundle (ExtractTextPlugin) 33 | * [x] React Router 34 | * [x] Redux 35 | * [x] Redux Thunk 36 | * [x] Redux Dev Tools 37 | * [x] Universal rendering 38 | * [x] Redux state prehydration 39 | * [x] Server support for router paths 40 | * [x] Webpack Environment plugin 41 | * [x] Dynamic imports (Loadable) 🎆 42 | * [x] Styleguidist 43 | * [x] i18next 44 | 45 | 46 | ### Prerequisites 47 | 48 | Yarn >= 1.0 49 | Node >= 8.0 50 | 51 | ``` 52 | yarn global add typescript tslint 53 | ``` 54 | 55 | ### App development 56 | Install dependencies, test if all glues together and run the development server 57 | 58 | ``` 59 | yarn install 60 | yarn test 61 | yarn start 62 | open http://localhost:3000 63 | ``` 64 | 65 | Test watch mode 66 | 67 | ``` 68 | yarn test:watch 69 | ``` 70 | 71 | ### Components development 72 | This project integrates styleguidist for components development 73 | ``` 74 | yarn styleguidist 75 | open http://localhost:6060 76 | ``` 77 | 78 | ### Production 79 | 80 | A simple production setup: headless tests, prod build and run server. 81 | Build puts artifacts into the `dist` directory and server should run from there 82 | 83 | ``` 84 | yarn test:phantom 85 | yarn build 86 | yarn server 87 | ``` 88 | 89 | 90 | ### SSR 91 | 92 | An example of the server-side rendering with the state prehydration. 93 | 94 | ``` 95 | yarn ssr-server 96 | ``` 97 | 98 | Todos: 99 | * [x] Check preload dynamic imports with Loadable on the server side and in tests 100 | * [ ] Hot reload of styles in the styleguidist (to avoid manual refresh css) 101 | * [ ] Consider hot reload of styles in the HMR mode 102 | * [ ] Loadable prefetch 103 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | App (dev - HMR) 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /index.prod.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | App (prod) 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var, strict */ 2 | 'use strict'; 3 | process.env.NODE_ENV = 'development'; 4 | const webpackConfig = require('./webpack.config.js')('dev'); //prod was working as well 5 | const merge = require('webpack-merge'); 6 | const webpack = require('webpack'); 7 | 8 | /** 9 | * We're overriding any loaders to achieve: 10 | * - removed css/less/sass style imports 11 | * - pure tsx compilation 12 | */ 13 | const webpackConfigModule = { 14 | rules: [ 15 | { 16 | test: /\.tsx?$/, 17 | use: [ 18 | 19 | { loader: 'ts-loader', 20 | options: { 21 | happyPackMode: true, 22 | transpileOnly: false 23 | } 24 | } 25 | ], 26 | exclude: [/node_modules/], 27 | }, 28 | { test: /\.(css|less|scss)$/, loader: 'ignore-loader' }, 29 | { 30 | test: /\.html$/, 31 | loader: 'raw-loader', 32 | exclude: /node_modules/ 33 | } 34 | ] 35 | }; 36 | 37 | module.exports = function(config) { 38 | // Documentation: https://karma-runner.github.io/0.13/config/configuration-file.html 39 | config.set({ 40 | basePath: '', 41 | frameworks: [ 'jasmine'], 42 | browsers: [ 'Chrome', 'PhantomJS' ], 43 | files: [ 44 | // This ensures we have the es6 shims in place and then loads all the tests 45 | 'test/main.js' 46 | ], 47 | port: 9876, 48 | logLevel: config.LOG_INFO, //config.LOG_DEBUG 49 | 50 | preprocessors: { 51 | 'test/main.js': [ 'webpack', 'sourcemap' ] 52 | }, 53 | 54 | 55 | webpack: { 56 | mode: 'development', 57 | devtool: 'inline-source-map', 58 | module: webpackConfigModule, 59 | resolve: webpackConfig.resolve 60 | }, 61 | 62 | webpackMiddleware: { 63 | quiet: true, 64 | stats: { 65 | colors: true 66 | } 67 | }, 68 | 69 | // reporter options 70 | mochaReporter: { 71 | colors: { 72 | success: 'bgGreen', 73 | info: 'cyan', 74 | warning: 'bgBlue', 75 | error: 'bgRed' 76 | } 77 | } 78 | }); 79 | }; -------------------------------------------------------------------------------- /module.d.ts: -------------------------------------------------------------------------------- 1 | declare var module: { 2 | hot: { 3 | accept: any; 4 | } 5 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "universal-typescript-react", 3 | "version": "1.0.3", 4 | "description": "React + TypeScript + Webpack 4 + Karma + Enzyme + HMR", 5 | "scripts": { 6 | "**** DEVELOPMENT": "****", 7 | "start": "webpack-dev-server --mode development --config webpack.config.dev.babel.js", 8 | "start:no-babel": "webpack-dev-server --mode development --config webpack.config.dev.js", 9 | "styleguidist": "styleguidist server", 10 | "test:chrome": "karma start --reporters nyan,junit --single-run=true --browsers Chrome", 11 | "test:watch": "karma start --reporters nyan --single-run=false --browsers Chrome", 12 | "lint": "tslint -c tslint.json 'src/**/*.{ts,tsx}'", 13 | "**** PROD BUILD SCRIPTS": "****", 14 | "test": "karma start --reporters mocha,junit --single-run=true --browsers PhantomJS", 15 | "prebuild": "rimraf dist", 16 | "build": "webpack --mode production --config webpack.config.js -p --env=prod", 17 | "**** SERVER SCRIPTS": "****", 18 | "server": "cd dist && python -m SimpleHTTPServer 8080", 19 | "ssr-server": "webpack --config webpack.config.server.js && node server/server.js" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/zdanowiczkonrad/universal-typescript-react.git" 24 | }, 25 | "keywords": [ 26 | "react", 27 | "reactjs", 28 | "typescript", 29 | "ts", 30 | "webpack", 31 | "hmr", 32 | "karma", 33 | "starter" 34 | ], 35 | "author": "Konrad Zdanowicz", 36 | "license": "MIT", 37 | "homepage": "https://github.com/zdanowiczkonrad/universal-typescript-react", 38 | "devDependencies": { 39 | "@types/enzyme": "^3.1.9", 40 | "@types/express": "^4.11.1", 41 | "@types/express-serve-static-core": "^4.11.1", 42 | "@types/i18next": "^8.4.3", 43 | "@types/i18next-browser-languagedetector": "^2.0.1", 44 | "@types/jasmine": "^2.8.6", 45 | "@types/jsdom": "^11.0.4", 46 | "@types/lodash": "^4.14.106", 47 | "@types/node": "^9.6.0", 48 | "@types/prop-types": "^15.5.2", 49 | "@types/react": "^16.0.40", 50 | "@types/react-dom": "^16.0.4", 51 | "@types/react-hot-loader": "^3.0.6", 52 | "@types/react-i18next": "^7.3.2", 53 | "@types/react-loadable": "^5.3.3", 54 | "@types/react-redux": "^5.0.15", 55 | "@types/react-router": "^4.0.23", 56 | "@types/react-router-config": "^1.0.6", 57 | "@types/react-router-dom": "^4.2.5", 58 | "@types/react-router-redux": "^5.0.13", 59 | "@types/redux-devtools": "^3.0.43", 60 | "@types/redux-devtools-dock-monitor": "^1.1.32", 61 | "@types/redux-devtools-log-monitor": "^1.0.33", 62 | "@types/serialize-javascript": "^1.3.2", 63 | "@types/webpack": "^4.1.2", 64 | "autoprefixer": "^8.2.0", 65 | "babel-core": "^6.26.0", 66 | "babel-loader": "^7.1.2", 67 | "babel-plugin-dynamic-import-node": "^1.2.0", 68 | "babel-plugin-transform-class-properties": "^6.24.1", 69 | "babel-polyfill": "^6.26.0", 70 | "babel-preset-env": "^1.6.1", 71 | "babel-preset-react": "^6.24.1", 72 | "copyfiles": "^2.0.0", 73 | "cross-env": "^5.1.4", 74 | "css-loader": "^0.28.11", 75 | "enzyme": "^3.3.0", 76 | "enzyme-adapter-react-16": "^1.1.1", 77 | "express": "^4.16.3", 78 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 79 | "fork-ts-checker-webpack-plugin": "^0.4.1", 80 | "html-loader": "^0.5.5", 81 | "html-webpack-plugin": "^3.1.0", 82 | "i18next": "^10.6.0", 83 | "i18next-browser-languagedetector": "^2.1.1", 84 | "ignore-loader": "^0.1.2", 85 | "jasmine": "^3.1.0", 86 | "jasmine-core": "^3.1.0", 87 | "jsdom": "^11.6.2", 88 | "json-loader": "^0.5.7", 89 | "karma": "^2.0.0", 90 | "karma-chrome-launcher": "^2.2.0", 91 | "karma-jasmine": "^1.1.1", 92 | "karma-junit-reporter": "^1.2.0", 93 | "karma-mocha-reporter": "^2.2.5", 94 | "karma-notify-reporter": "^1.0.1", 95 | "karma-nyan-reporter": "^0.2.5", 96 | "karma-phantomjs-launcher": "^1.0.4", 97 | "karma-sourcemap-loader": "^0.3.7", 98 | "karma-webpack": "^3.0.0", 99 | "less": "^3.0.1", 100 | "less-loader": "^4.1.0", 101 | "node-mv": "^0.1.3", 102 | "node-sass": "^4.7.2", 103 | "phantomjs-prebuilt": "^2.1.16", 104 | "postcss-loader": "^2.1.3", 105 | "prop-types": "^15.5.7-alpha.1", 106 | "raw-loader": "^0.5.1", 107 | "react-docgen-typescript": "^1.2.6", 108 | "react-hot-loader": "^4.0.0", 109 | "react-i18next": "^7.5.1", 110 | "react-router": "^4.2.0", 111 | "react-router-config": "^1.0.0-beta.4", 112 | "react-router-dom": "^4.2.2", 113 | "react-styleguidist": "^7.0.3", 114 | "redux-devtools": "^3.4.1", 115 | "redux-devtools-dock-monitor": "^1.1.3", 116 | "redux-devtools-log-monitor": "^1.4.0", 117 | "redux-thunk": "^2.2.0", 118 | "rimraf": "^2.6.2", 119 | "sass-loader": "^6.0.7", 120 | "serialize-javascript": "^1.4.0", 121 | "style-loader": "^0.20.3", 122 | "ts-loader": "^4.1.0", 123 | "ts-node": "^5.0.1", 124 | "tslint": "^5.9.1", 125 | "tslint-react": "^3.5.1", 126 | "typescript": "^2.7.2", 127 | "webpack": "^4.20.2", 128 | "webpack-cli": "^3.1.1", 129 | "webpack-dev-server": "^3.1.1", 130 | "webpack-merge": "^4.1.2" 131 | }, 132 | "dependencies": { 133 | "es6-promise": "^4.2.4", 134 | "lodash": "^4.17.5", 135 | "npm": "^5.7.1", 136 | "react": "^16.2.0", 137 | "react-dom": "16.2.1", 138 | "react-loadable": "^5.3.1", 139 | "react-redux": "5.0.6", 140 | "react-router-redux": "^5.0.0-alpha.9", 141 | "redux": "3.7.2" 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const autoprefixer = require('autoprefixer') 2 | module.exports = { 3 | // plugins: { 4 | // autoprefixer: { browsers: ['cover 99.5%']} // this is commented, as there is a problem with css source maps 5 | // } 6 | }; -------------------------------------------------------------------------------- /require.d.ts: -------------------------------------------------------------------------------- 1 | declare var require: { 2 | (path: string): T; 3 | (paths: string[], callback: (...modules: any[]) => void): void; 4 | ensure: (paths: string[], callback: (require: (path: string) => T) => void) => void; 5 | }; -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 20px; 3 | border-radius: 5px; 4 | } 5 | 6 | 7 | ::placeholder { 8 | display: block; 9 | } -------------------------------------------------------------------------------- /src/App.less: -------------------------------------------------------------------------------- 1 | @import './Config'; 2 | 3 | body { 4 | color: #aca; 5 | display: flex; 6 | } 7 | -------------------------------------------------------------------------------- /src/App.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Monaco', serif; 3 | } -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { hot } from 'react-hot-loader'; 2 | // Load React AFTER hot loader 3 | import * as React from 'react'; 4 | import Layout from '@/Layout'; 5 | import '@/App.css'; 6 | import '@/App.less'; 7 | import '@/App.scss'; 8 | import { Provider } from 'react-redux'; 9 | import { store } from '@/store'; 10 | import { Route } from 'react-router'; 11 | import { ConnectedRouter } from 'react-router-redux'; 12 | import { history } from '@/history'; 13 | import { config } from '@/config'; 14 | import { DevTools } from '@/DevTools'; 15 | import i18n from '@/i18n'; 16 | import { I18nextProvider } from 'react-i18next'; 17 | import DemoContainer from '@/components/demo/Demo'; 18 | import Counter from '@/Counter'; 19 | import { MooProvider } from '@/context/Moo'; 20 | 21 | // If you use React Router, make this component 22 | // render with your routes. Currently, 23 | // only synchronous routes are hot reloaded, and 24 | // you will see a warning from on every reload. 25 | // You can ignore this warning. For details, see: 26 | // https://github.com/reactjs/react-router/issues/2182 27 | export const App = () => ( 28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 | 36 |
37 |
38 | {config.isDevelopment && } 39 |
40 |
41 |
42 |
43 | ); 44 | 45 | /* this required to export a root component wrapped in a hot loader function */ 46 | /* else, HMR will not preserve local component state */ 47 | export default hot(module)(App); 48 | -------------------------------------------------------------------------------- /src/Config.less: -------------------------------------------------------------------------------- 1 | @assets: ''; 2 | body { 3 | background-image: url('@{assets}/test.png'); 4 | } -------------------------------------------------------------------------------- /src/Counter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { sum } from '@/calculator/Calculator'; 3 | import { PreloadedComponent } from '@/PreloadedComponent'; 4 | 5 | export default class Counter extends React.Component<{}, { counter: number }> { 6 | interval: number; 7 | promise: Promise; 8 | constructor(props: {}) { 9 | super(props); 10 | this.state = { counter: 0 }; 11 | } 12 | 13 | componentDidMount() { 14 | this.promise = new Promise((resolve) => { 15 | resolve(); 16 | }); 17 | this.interval = window.setInterval(this.tick, 1000); 18 | } 19 | 20 | tick = () => { 21 | this.setState({ counter: sum(this.state.counter, 2) }); 22 | } 23 | 24 | componentWillUnmount() { 25 | clearInterval(this.interval); 26 | } 27 | 28 | render() { 29 | return ( 30 |
31 |

Counter: {this.state.counter}

32 | 33 |
34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/DevTools.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // Exported from redux-devtools 4 | import { createDevTools } from 'redux-devtools'; 5 | 6 | // Monitors are separate packages, and you can make a custom one 7 | import LogMonitor from 'redux-devtools-log-monitor'; 8 | import DockMonitor from 'redux-devtools-dock-monitor'; 9 | 10 | // createDevTools takes a monitor and produces a DevTools component 11 | const DevTools = createDevTools( 12 | // Monitors are individually adjustable with props. 13 | // Consult their repositories to learn about those props. 14 | // Here, we put LogMonitor inside a DockMonitor. 15 | // Note: DockMonitor is visible by default. 16 | 21 | 22 | 23 | ); 24 | 25 | export { DevTools }; 26 | -------------------------------------------------------------------------------- /src/Layout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Menu from '@/Menu'; 3 | 4 | const Layout: React.SFC = ({ children }) => ( 5 |
6 |

Hello, React and TypeScript and Hot module reload

7 | 8 | {children} 9 |
10 | ); 11 | 12 | export default Layout; 13 | -------------------------------------------------------------------------------- /src/LazyComponent.less: -------------------------------------------------------------------------------- 1 | .counter { 2 | text-decoration: underline; 3 | } -------------------------------------------------------------------------------- /src/LazyComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import '@/LazyComponent.less'; 3 | import { connect } from 'react-redux'; 4 | 5 | export class LazyComponent extends React.Component<{ appName: string }, {}> { 6 | render() { 7 | return

{this.props.appName} Hello from a lazily loaded component!

; 8 | } 9 | } 10 | 11 | export default connect((state: any) => { 12 | return ({appName: state.app.appName}); 13 | })(LazyComponent); 14 | -------------------------------------------------------------------------------- /src/Menu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Dispatch, connect } from 'react-redux'; 3 | import { push } from 'react-router-redux'; 4 | import { PreloadedComponentLoader } from '@/PreloadedComponent'; 5 | 6 | /* tslint:disable:no-console */ 7 | const deferredOpenDemoPageAction = () => (dispatch: Dispatch, getState: () => any) => { 8 | console.log('thunked action demo'); 9 | console.log(getState()); 10 | setTimeout( 11 | () => { 12 | console.log('ok, happened!'); 13 | dispatch(push('/demo')); 14 | }, 15 | 1000); 16 | }; 17 | 18 | export class Menu extends React.Component<{ dispatch?: Dispatch }, {}> { 19 | preloadDemoComponent = () => { 20 | PreloadedComponentLoader.preload(); 21 | } 22 | 23 | render() { 24 | const { dispatch } = this.props; 25 | return ( 26 | menu: 27 | dispatch(push('/counter'))}>Counter_ 28 | dispatch(deferredOpenDemoPageAction())}>Demo 29 | 30 | ); 31 | } 32 | } 33 | 34 | export default connect()(Menu); 35 | -------------------------------------------------------------------------------- /src/Polyfill.tsx: -------------------------------------------------------------------------------- 1 | import { polyfill } from 'es6-promise'; 2 | 3 | export { polyfill }; -------------------------------------------------------------------------------- /src/PreloadedComponent.tsx: -------------------------------------------------------------------------------- 1 | import Loadable from 'react-loadable'; 2 | import * as React from 'react'; 3 | 4 | export const PreloadedComponentLoader = Loadable({ 5 | loader: (() => { 6 | return import('./LazyComponent') as any; 7 | }), 8 | loading() { 9 | return
Loading default component...
; 10 | } 11 | } ); 12 | 13 | export class PreloadedComponent extends React.Component<{}, {}> { 14 | render() { 15 | return ; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/StyleguidistWrapper.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { I18nextProvider } from 'react-i18next'; 3 | import { Provider } from 'react-redux'; 4 | import i18n from '@/i18n'; 5 | import { store } from '@/store'; 6 | 7 | export default class Wrapper extends React.Component<{}, {}> { 8 | render() { 9 | return ( 10 | 11 | 12 | {this.props.children} 13 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/calculator/Calculator.ts: -------------------------------------------------------------------------------- 1 | export const sum = (a: number, b: number) => { 2 | return a + b; 3 | }; 4 | -------------------------------------------------------------------------------- /src/components/demo/Demo.less: -------------------------------------------------------------------------------- 1 | .demo { 2 | border: 4px #eee solid; 3 | } -------------------------------------------------------------------------------- /src/components/demo/Demo.md: -------------------------------------------------------------------------------- 1 | This a basic demo component: 2 | 3 | **Import** 4 | 5 | `import Demo from '@/components/demo/Demo';` 6 | 7 | **Simple usage** 8 | 9 | ```jsx 10 | 11 | ``` 12 | 13 | **This is a demo component with subtitle** 14 | 15 | ```jsx 16 | 17 | ``` 18 | -------------------------------------------------------------------------------- /src/components/demo/Demo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './Demo.less'; 3 | import { TranslationFunction } from 'i18next'; 4 | import { translate } from 'react-i18next'; 5 | import { moo, IMooContext } from '@/context/Moo'; 6 | 7 | export interface IDemoProps { 8 | /** subtitle displayed underneath the demo components. use when it is needed */ 9 | subtitle?: string; 10 | /** t translation */ 11 | t?: TranslationFunction; 12 | } 13 | 14 | /** 15 | * Demo component 16 | */ 17 | @moo 18 | export class Demo extends React.Component { 19 | context: IMooContext; 20 | render() { 21 | /* tslint:disable:no-console */ 22 | console.log(this.context); 23 | if (this.context.moo) { 24 | this.context.moo.sayMoo('Tim'); 25 | } 26 | /* tslint:enable:no-console */ 27 | return ( 28 |
29 |

Demo component

30 | {this.props.t('hello')} 31 | {this.props.subtitle} 32 |
33 | ); 34 | } 35 | } 36 | 37 | /** 38 | * Important. always split the default export of the component with a const variable 39 | * instead of inlining it to help Styleguidist interpret props & methods. 40 | * 41 | * A rule of a thumb for components is to always export as default the component 42 | * to have the consistency (but this is annoying) 43 | */ 44 | const DemoContainer = translate('translations')(Demo); 45 | export default DemoContainer; 46 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | interface IConfig { 2 | isDevelopment: boolean; 3 | /** Runtime environment of the app */ 4 | env: 'dev' | 'stage' | 'prod'; 5 | /** Is this a browser-targeted bundle? */ 6 | isBrowser: boolean; 7 | /** Is this a test run? */ 8 | isTest: boolean; 9 | } 10 | 11 | export const config: IConfig = { 12 | env: 'dev', 13 | isDevelopment: process.env.NODE_ENV !== 'production', 14 | isTest: process.env.IS_TEST === 'true', 15 | isBrowser: process.env.IS_BROWSER === 'true' 16 | }; 17 | 18 | /* tslint:disable:no-console */ 19 | console.debug('Running the app with the config', config); 20 | -------------------------------------------------------------------------------- /src/context/Moo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | // This is a deprecated context usage, but ok for now (05-04-2018) 3 | import PropTypes from 'prop-types'; 4 | 5 | class Moo { 6 | sayMoo(name?: string): void { 7 | /* tslint:disable:no-console */ 8 | console.log(name ? 'Moo!' : `Moo, ${name}!`); 9 | /* tslint:enable:no-console */ 10 | } 11 | } 12 | 13 | const MOO = new Moo(); 14 | 15 | /** 16 | * Autowire context types when used like: 17 | * 18 | * @moo 19 | * class Component { 20 | * this.context: IMooContext; 21 | * ... 22 | * this.context.moo.sayMoo(); 23 | * } 24 | * 25 | * Requires components to be wrapped inside the MooProvider 26 | */ 27 | export function moo(target: any) { 28 | target.contextTypes = target.contextTypes || {}; 29 | target.contextTypes['moo'] = PropTypes.object; 30 | } 31 | 32 | export interface IMooContext { 33 | moo: Moo; 34 | } 35 | 36 | export class MooProvider extends React.Component<{}, {}> { 37 | getChildContext() { 38 | return { 39 | moo: MOO 40 | } as IMooContext; 41 | } 42 | 43 | static childContextTypes = { 44 | moo: PropTypes.object 45 | }; 46 | 47 | render() { 48 | return this.props.children; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/history.ts: -------------------------------------------------------------------------------- 1 | import createBrowserHistory from 'history/createBrowserHistory'; 2 | import createMemoryHistory from 'history/createMemoryHistory'; 3 | import { config } from '@/config'; 4 | 5 | export const history = config.isBrowser ? 6 | createBrowserHistory() : 7 | createMemoryHistory(); 8 | -------------------------------------------------------------------------------- /src/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import LanguageDetector from 'i18next-browser-languagedetector'; 3 | 4 | i18n 5 | .use(LanguageDetector) 6 | .init({ 7 | // we init with resources 8 | resources: { 9 | en: { 10 | translations: { 11 | 'hello': 'Welcome to React.js', 12 | } 13 | }, 14 | pl: { 15 | translations: { 16 | 'hello': 'Witaj w React.js', 17 | } 18 | } 19 | }, 20 | fallbackLng: 'en', 21 | debug: true, 22 | 23 | // have a common namespace used around the full app 24 | ns: ['translations'], 25 | defaultNS: 'translations', 26 | 27 | keySeparator: false, // we use content as keys 28 | 29 | interpolation: { 30 | escapeValue: false, // not needed for react!! 31 | formatSeparator: ',' 32 | }, 33 | 34 | react: { 35 | wait: false 36 | } 37 | }); 38 | 39 | export default i18n; -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { polyfill } from '@/Polyfill'; 4 | import App from '@/App'; 5 | 6 | polyfill(); 7 | 8 | const rootEl = document.getElementById('root'); 9 | 10 | render(, rootEl); 11 | -------------------------------------------------------------------------------- /src/reducers.ts: -------------------------------------------------------------------------------- 1 | import { routerReducer } from 'react-router-redux'; 2 | import { Reducer, combineReducers, Action } from 'redux'; 3 | 4 | export const rootReducer: Reducer = combineReducers({ 5 | app: (prevState = { appName: 'Yay, Redux!'}, action: Action) => { 6 | if (action.type === 'SERVER_PREHYDRATION') { 7 | return { 8 | appName: 'Yay, Redux from prehydration!' 9 | }; 10 | } else { 11 | return prevState; 12 | } 13 | }, 14 | router: routerReducer 15 | }); 16 | -------------------------------------------------------------------------------- /src/server.tsx: -------------------------------------------------------------------------------- 1 | /* Based on 2 | https://github.com/lith-light-g/universal-react-redux-typescript-starter-kit 3 | */ 4 | 5 | import * as React from 'react'; 6 | import { renderToString, renderToStaticMarkup } from 'react-dom/server'; 7 | import * as express from 'express'; 8 | import * as serialize from 'serialize-javascript'; 9 | // import * as webpackDevMiddleware from 'webpack-dev-middleware'; 10 | // import * as webpackHotMiddleware from 'webpack-hot-middleware'; 11 | 12 | import { Express, Response, Request } from 'express-serve-static-core'; 13 | // import { StaticRouter } from 'react-router-dom'; 14 | // import { matchRoutes, renderRoutes, MatchedRoute } from 'react-router-config'; 15 | import { App } from '../src/App'; 16 | import { store } from './store'; 17 | 18 | const indexFile: string = require('../dist/index.html'); 19 | const app: Express = express(); 20 | const port = process.env.PORT || 3000; 21 | import { JSDOM } from 'jsdom'; 22 | import { push } from 'react-router-redux'; 23 | 24 | const indexDocument = new JSDOM(indexFile).window.document; 25 | const scripts = Array.from(indexDocument.querySelectorAll('script')).map(script => script.src); 26 | const styles = (Array.from(indexDocument.querySelectorAll('link[rel=stylesheet]')) as HTMLLinkElement[]).map((css => css.href)); 27 | 28 | app.use(express.static('./dist')); 29 | 30 | app.get('/:routerPath', (req: Request, res: Response) => { 31 | store.dispatch({ 32 | type: 'SERVER_PREHYDRATION' 33 | }); 34 | // set initial route to the store 35 | store.dispatch(push('/' + req.params.routerPath)); 36 | const reactAppElement: string = renderToString(( 37 | 38 | )); 39 | 40 | // if redirect has been used 41 | // if (context.url) { 42 | // res.redirect(302, context.url); 43 | // return; 44 | // } 45 | 46 | res.send(`${renderToStaticMarkup(( 47 | 48 | 49 | App (prerender) 50 | {styles.map(style => )} 51 | 52 | 53 |
54 |