├── .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 | [](https://travis-ci.org/zdanowiczkonrad/universal-typescript-react) [](https://badge.fury.io/js/universal-typescript-react) [](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 |
58 | {scripts.map((script: string) =>