├── .browserslistrc ├── .cz-config.js ├── .editorconfig ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── __mocks__ └── fileMock.js ├── babel.config.js ├── commitlint.config.js ├── jest.config.js ├── nodemon.json ├── package-lock.json ├── package.json ├── src ├── client │ ├── index.tsx │ └── serviceWorker.ts ├── core │ ├── api │ │ ├── index.ts │ │ └── request.ts │ ├── app.tsx │ ├── assets │ │ ├── images │ │ │ └── logo.svg │ │ └── styles │ │ │ ├── fonts.scss │ │ │ ├── global.scss │ │ │ ├── mixins.scss │ │ │ └── variables.scss │ ├── common │ │ ├── config.ts │ │ ├── preload.ts │ │ ├── reducer.ts │ │ ├── routes.tsx │ │ ├── routing.tsx │ │ └── store.ts │ ├── features │ │ └── common │ │ │ ├── effects.ts │ │ │ ├── index.ts │ │ │ ├── page.tsx │ │ │ ├── reducer.ts │ │ │ ├── routes.ts │ │ │ ├── selectors.ts │ │ │ └── types.ts │ └── ui │ │ ├── atoms │ │ ├── Align │ │ │ ├── Align.scss │ │ │ ├── Align.tsx │ │ │ └── index.ts │ │ ├── Button │ │ │ ├── Button.scss │ │ │ ├── Button.tsx │ │ │ └── index.ts │ │ ├── ErrorMessage │ │ │ ├── ErrorMessage.scss │ │ │ ├── ErrorMessage.tsx │ │ │ └── index.ts │ │ ├── Logo │ │ │ ├── Logo.scss │ │ │ ├── Logo.tsx │ │ │ └── index.ts │ │ ├── PreviewImage │ │ │ ├── PreviewImage.scss │ │ │ ├── PreviewImage.tsx │ │ │ └── index.ts │ │ └── index.ts │ │ ├── molecules │ │ ├── Header │ │ │ ├── Header.scss │ │ │ ├── Header.tsx │ │ │ └── index.ts │ │ └── index.ts │ │ └── templates │ │ ├── PageTemplate │ │ ├── PageTemplate.scss │ │ ├── PageTemplate.tsx │ │ └── index.ts │ │ └── index.ts └── server │ ├── exitHandler.ts │ ├── index.ts │ ├── lib │ ├── render │ │ ├── index.ts │ │ ├── renderApp.tsx │ │ └── renderHtml.tsx │ └── utils │ │ ├── __mocks__ │ │ └── matchedRoutes.ts │ │ ├── fillStore │ │ ├── fillStore.test.ts │ │ ├── fillStore.ts │ │ └── index.ts │ │ ├── getGroupedAssets │ │ ├── getGroupedAssets.test.ts │ │ ├── getGroupedAssets.ts │ │ └── index.ts │ │ ├── getPreloadActionsFromRoutes │ │ ├── getPreloadActionsFromRoutes.test.ts │ │ ├── getPreloadActionsFromRoutes.ts │ │ └── index.ts │ │ ├── getRequire │ │ ├── getRequire.test.ts │ │ ├── getRequire.ts │ │ └── index.ts │ │ └── index.ts │ ├── middlewares │ └── assetsParser.ts │ └── router.ts ├── tsconfig.json ├── tslint.json ├── types ├── global │ └── index.d.ts ├── redux │ └── index.d.ts └── window │ └── index.d.ts ├── webpack.config.js └── webpack ├── common.js ├── paths.js ├── utils ├── createSelectorName.js └── index.js ├── webpack.client.development.js ├── webpack.client.production.js ├── webpack.server.development.js └── webpack.server.production.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | # Browsers that we support 2 | 3 | [production staging] 4 | >1%, 5 | not dead, 6 | not ie <= 11, 7 | not op_mini all 8 | 9 | [development] 10 | last 1 safari version, 11 | last 1 chrome version, 12 | last 1 firefox version 13 | -------------------------------------------------------------------------------- /.cz-config.js: -------------------------------------------------------------------------------- 1 | const glob = require('glob'); 2 | 3 | /** 4 | * @param {string} pattern 5 | * @param {Function} [fn] 6 | */ 7 | const globMap = (pattern, fn) => 8 | glob 9 | .sync(pattern) 10 | .map(fn || (path => path)) 11 | .map(path => path.replace(/\/$/, '')); 12 | 13 | /** 14 | * Check `path` to not include substring in `variants` 15 | * @param {string[]} variants 16 | * @return {function(*): *} 17 | */ 18 | 19 | const exclude = variants => path => variants.every(variant => !path.includes(variant)); 20 | 21 | module.exports = { 22 | types: [ 23 | { value: 'feat', name: '🎸 feat: A new feature' }, 24 | { value: 'fix', name: '🐛 fix: A bug fix' }, 25 | { value: 'refactor', name: '💡 refactor: A code change that neither fixes a bug or adds a feature' }, 26 | { value: 'style', name: '💄 style: Markup, white-space, formatting, missing semi-colons...' }, 27 | { value: 'test', name: '💍 test: Adding missing tests' }, 28 | { value: 'chore', name: '🤖 chore: Build process or auxiliary tool changes' }, 29 | { value: 'wip', name: '🕯 wip: Work in progress' }, 30 | { value: 'docs', name: '✏️ docs: Documentation only changes' }, 31 | { value: 'revert', name: '🐰 revert: Revert to a commit' }, 32 | { value: 'perf', name: '💪 perf: A code change that improves performance"' } 33 | ], 34 | allowCustomScopes: true, 35 | skipQuestions: ['footer'], 36 | allowBreakingChanges: ['fix', 'feat', 'revert', 'refactor'], 37 | scopes: [].concat( 38 | 'client', 39 | globMap('src/client/*/', path => path.replace(/src\//, '')), 40 | 'core', 41 | globMap('src/core/*/', path => path.replace(/src\//, '')).filter(exclude(['core/features', 'core/ui'])), 42 | 'core/features', 43 | globMap('src/core/features/*/', path => path.replace(/src\//, '')), 44 | 'core/ui', 45 | globMap('src/core/ui/*/', path => path.replace(/src\//, '')), 46 | 'server', 47 | globMap('src/server/*/', path => path.replace(/src\//, '')), 48 | 'types', 49 | globMap('types/*/') 50 | ) 51 | }; 52 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset=utf-8 3 | end_of_line = lf 4 | insert_final_newline=true 5 | indent_style=space 6 | indent_size=2 7 | 8 | [*.md] 9 | max_line_length = off 10 | trim_trailing_whitespace = false 11 | insert_final_newline = false 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /tmp 4 | /out-tsc 5 | 6 | # dependencies 7 | /node_modules 8 | /storybook-static 9 | 10 | # IDEs and editors 11 | /.idea 12 | .project 13 | .classpath 14 | .c9/ 15 | *.launch 16 | .settings/ 17 | *.sublime-workspace 18 | 19 | # IDE - VSCode 20 | .vscode/* 21 | !.vscode/settings.json 22 | !.vscode/tasks.json 23 | !.vscode/launch.json 24 | !.vscode/extensions.json 25 | 26 | # misc 27 | /.sass-cache 28 | /connect.lock 29 | /coverage 30 | /libpeerconnection.log 31 | npm-debug.log* 32 | yarn-debug.log* 33 | yarn-error.log* 34 | testem.log 35 | 36 | # System Files 37 | .DS_Store 38 | Thumbs.db 39 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "semi": true, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": true, 8 | "arrowParens": "avoid", 9 | "proseWrap": "always", 10 | "printWidth": 120 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React TypeScript Express SSR example 2 | 3 | > React ssr exmaple with typescript, babel, css-modules, react-router, redux, redux-thunk, ramda, webpack 4. 4 | 5 | [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 6 | 7 | You can read more about the organizational strategy used in this app in 8 | [this Medium post](https://medium.com/@nate_wang/feature-oriented-architecture-for-web-applications-2b48e358afb0), or 9 | [this post](https://jaysoo.ca/2016/02/28/organizing-redux-application/). 10 | 11 | ## Contains 12 | 13 | - [x] [React](https://facebook.github.io/react/) 16.8 14 | - [x] [React Router](https://github.com/ReactTraining/react-router) 5.0 15 | - [x] [Redux](https://github.com/reactjs/redux) 4 16 | - [x] [Redux-Thunk](https://github.com/gaearon/redux-thunk) 2.3 17 | - [x] [react-hot-loader](https://github.com/gaearon/react-hot-loader) 4.11 18 | - [x] [Ramda](https://github.com/ramda/ramda) 0.26 19 | - [x] [express](https://github.com/expressjs/express) 4.17 20 | - [x] [babel](https://github.com/babel/babel) 7 21 | - [x] [webpack](https://github.com/webpack/webpack) 4.33 22 | - [x] [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) 3.7 23 | - [x] [webpack-hot-middleware](https://github.com/webpack-contrib/webpack-hot-middleware) 2.25 24 | 25 | ## Aliases 26 | 27 | - `@client/*` resolves to `./src/client/*` 28 | - `@server/*` resolves to `./src/server/*` 29 | - `@core/*` resolves to `./src/core/*` 30 | 31 | ## Routing 32 | 33 | Routes in project are objects with the same properties as a `` with a couple differences: 34 | 35 | - the only render prop it accepts is `component` (no `render` or `children`) 36 | - introduces the `routes` key for sub routes 37 | - introduces the `preloadActions` key for preFetch on server 38 | - Consumers are free to add any additional props they'd like to a route, you can access `props.route` inside the 39 | `component`, this object is a reference to the object used to render and match. 40 | - accepts `key` prop to prevent remounting component when transition was made from route with the same component and 41 | same `key` prop 42 | 43 | ```js 44 | const routes = [ 45 | { 46 | component: Root, 47 | preloadActions: someAction('someProp'), // preloadActions: {type: 'someAction', payload: someProps} 48 | routes: [ 49 | { 50 | path: '/', 51 | exact: true, 52 | component: Home, 53 | preloadActions: [someAction('someProp'), someSecondAction('someProps')] 54 | }, 55 | { 56 | path: '/child/:id', 57 | component: Child, 58 | routes: [ 59 | { 60 | path: '/child/:id/grand-child', 61 | component: GrandChild 62 | } 63 | ] 64 | } 65 | ] 66 | } 67 | ]; 68 | ``` 69 | 70 | **Note**: Just like ``, relative paths are not (yet) supported. When it is supported there, it will be supported 71 | here. 72 | 73 | ## Using CSS 74 | 75 | It uses the usual SCSS css modules. You can find more information [here](https://github.com/css-modules/css-modules) 76 | 77 | ## Render instead of hydrate in development mode 78 | 79 | To be able to reload the page and see the latest code changes, you must set the "localStorage.useRender" value in 80 | development mode (the hydrate method will be replaced with the render) 81 | 82 | ## Setup 83 | 84 | ```bash 85 | $ npm install 86 | ``` 87 | 88 | ## Running 89 | 90 | Start express development server 91 | 92 | ```bash 93 | $ npm run start 94 | ``` 95 | 96 | ## Build 97 | 98 | Build client for production 99 | 100 | ```bash 101 | $ npm run build:client 102 | ``` 103 | 104 | Build server for production 105 | 106 | ```bash 107 | $ npm run build:server 108 | ``` 109 | 110 | Or simply 111 | 112 | ```bash 113 | $ npm run build 114 | ``` 115 | 116 | Build server and client for production 117 | 118 | ## Analyze 119 | 120 | Vendors Size (all node_modules) 121 | 122 | ```bash 123 | $ npm run analyze:vendors 124 | ``` 125 | 126 | Bundle Size (all exclude node_modules) 127 | 128 | ```bash 129 | $ npm run analyze:bundle 130 | ``` 131 | 132 | Or simply 133 | 134 | ```bash 135 | $ npm run analyze 136 | ``` 137 | 138 | To see the size and bundle and vendors 139 | -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const { compilerOptions } = require('./tsconfig'); 2 | 3 | const pathsEntries = Object.entries(compilerOptions.paths); 4 | 5 | /** 6 | * @param glob path | alias name 7 | * @return {string} path without glob pattern 8 | */ 9 | const removeGlobPattern = glob => glob.replace('/*', ''); 10 | 11 | /** 12 | * @param name alias name 13 | * @return {string} formated name (for use by babel) 14 | */ 15 | const prepareName = name => removeGlobPattern(name); 16 | 17 | /** 18 | * @param path alias path 19 | * @return {string} formated path (for use by babel) 20 | */ 21 | const preparePath = ([path]) => `./${compilerOptions.baseUrl}/${removeGlobPattern(path)}`; 22 | 23 | const alias = pathsEntries.reduce((accum, [name, path]) => ({ ...accum, [prepareName(name)]: preparePath(path) }), {}); 24 | 25 | module.exports = function(api) { 26 | const { NODE_ENV, TARGET } = process.env; 27 | 28 | const isProduction = NODE_ENV === 'production'; 29 | const isServer = TARGET === 'server'; 30 | 31 | api.cache(() => `${NODE_ENV}${TARGET}`); 32 | 33 | // client settings in .browserslistrc 34 | const targets = isServer ? { node: 'current' } : undefined; 35 | 36 | const presets = [ 37 | ['@babel/preset-env', { targets, loose: true, modules: false, useBuiltIns: 'usage', corejs: 3 }], 38 | '@babel/preset-typescript', 39 | '@babel/preset-react' 40 | ]; 41 | 42 | const plugins = [ 43 | ['@babel/plugin-proposal-class-properties', { loose: true }], 44 | ['babel-plugin-module-resolver', { alias }], 45 | ['@babel/plugin-transform-runtime'] 46 | ]; 47 | 48 | if (isProduction) { 49 | plugins.push(['@babel/plugin-transform-react-inline-elements']); 50 | } 51 | 52 | return { 53 | presets, 54 | plugins 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'type-case': [2, 'always', 'lower-case'], 5 | 'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'revert', 'perf', 'wip']] 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest/utils'); 2 | 3 | const { compilerOptions } = require('./tsconfig'); 4 | 5 | // For a detailed explanation regarding each configuration property, visit: 6 | // https://jestjs.io/docs/en/configuration.html 7 | 8 | module.exports = { 9 | collectCoverage: false, 10 | coverageThreshold: { 11 | global: { 12 | lines: 70, 13 | branches: 70, 14 | functions: 80, 15 | statements: -10 16 | } 17 | }, 18 | globals: { 19 | 'ts-jest': { 20 | tsConfig: 'tsconfig.json' 21 | } 22 | }, 23 | moduleFileExtensions: ['ts', 'tsx', 'js'], 24 | moduleDirectories: ['node_modules', 'src'], 25 | moduleNameMapper: { 26 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 27 | '/__mocks__/fileMock.js', 28 | '\\.(css|scss)$': 'identity-obj-proxy', 29 | ...pathsToModuleNameMapper(compilerOptions.paths) 30 | }, 31 | preset: 'ts-jest', 32 | testEnvironment: 'node', 33 | testMatch: ['**/__tests__/*.+(ts|js)', '**/*.test.(ts|js)'], 34 | transform: { 35 | '^.+\\.(ts|tsx)$': 'ts-jest' 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ext": "ts tsx", 3 | "ignore": ["**/*.(test|spec).(ts|js)"], 4 | "watch": ["src/server/**/*.ts", "src/server/**/*.tsx"], 5 | "exec": "cross-env TARGET=server webpack && node dist/index.js" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-typescript-express-ssr-example", 3 | "version": "1.0.0", 4 | "description": "A starter for a project using TypeScript ExpressJs and ReactJs with server side rendering.", 5 | "scripts": { 6 | "test": "run-s test:*", 7 | "test:code": "jest", 8 | "test:types": "tsc --noEmit", 9 | "test:lint": "tslint --project .", 10 | "analyze": "run-p analyze:*", 11 | "analyze:server": "source-map-explorer dist/index.*", 12 | "analyze:bundle": "source-map-explorer dist/bundle.*", 13 | "analyze:vendors": "source-map-explorer dist/vendors.*", 14 | "prestart": "rimraf dist", 15 | "prebuild": "rimraf dist", 16 | "start": "cross-env NODE_ENV=development PORT=3001 nodemon", 17 | "server": "cross-env NODE_ENV=production PORT=8081 node dist/index.js", 18 | "build": "run-p build:*", 19 | "build:client": "cross-env NODE_ENV=production TARGET=client webpack", 20 | "build:server": "cross-env NODE_ENV=production TARGET=server webpack" 21 | }, 22 | "publishConfig": { 23 | "registry": "https://registry.npmjs.org" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/weyheyhey/react-typescript-express-ssr-example.git" 28 | }, 29 | "keywords": [ 30 | "react-hot-loader", 31 | "boilerplate", 32 | "css-modules", 33 | "typescript", 34 | "webpack", 35 | "express", 36 | "react", 37 | "redux", 38 | "ssr" 39 | ], 40 | "author": "weyheyhey", 41 | "license": "Unlicense", 42 | "dependencies": { 43 | "@hot-loader/react-dom": "^16.8.6", 44 | "classnames": "^2.2.6", 45 | "core-js": "^3.1.3", 46 | "express": "^4.17.1", 47 | "helmet": "^3.18.0", 48 | "history": "^4.9.0", 49 | "isomorphic-fetch": "^2.2.1", 50 | "ramda": "^0.26.1", 51 | "react": "^16.8.6", 52 | "react-dom": "^16.8.6", 53 | "react-helmet": "^5.2.1", 54 | "react-hot-loader": "^4.11.0", 55 | "react-redux": "^7.1.0", 56 | "react-router": "^5.0.1", 57 | "react-router-config": "^5.0.1", 58 | "react-router-dom": "^5.0.1", 59 | "redux": "^4.0.1", 60 | "redux-actions": "^2.6.5", 61 | "redux-thunk": "2.3.0", 62 | "reselect": "^4.0.0", 63 | "serialize-javascript": "^2.1.1" 64 | }, 65 | "devDependencies": { 66 | "@babel/core": "^7.4.5", 67 | "@babel/plugin-proposal-class-properties": "^7.4.4", 68 | "@babel/plugin-transform-react-inline-elements": "^7.2.0", 69 | "@babel/plugin-transform-runtime": "^7.4.4", 70 | "@babel/preset-env": "^7.4.5", 71 | "@babel/preset-react": "^7.0.0", 72 | "@babel/preset-typescript": "^7.3.3", 73 | "@commitlint/cli": "^8.0.0", 74 | "@commitlint/config-conventional": "^8.0.0", 75 | "@types/classnames": "^2.2.8", 76 | "@types/express": "^4.17.0", 77 | "@types/helmet": "^0.0.43", 78 | "@types/jest": "^24.0.13", 79 | "@types/node": "^12.0.8", 80 | "@types/ramda": "^0.26.9", 81 | "@types/react": "^16.8.19", 82 | "@types/react-dom": "^16.8.4", 83 | "@types/react-helmet": "^5.0.8", 84 | "@types/react-redux": "^7.0.9", 85 | "@types/react-router": "^5.0.1", 86 | "@types/react-router-config": "^5.0.0", 87 | "@types/react-router-dom": "^4.3.3", 88 | "@types/redux-actions": "^2.6.1", 89 | "@types/redux-mock-store": "^1.0.1", 90 | "@types/serialize-javascript": "^1.5.0", 91 | "@types/webpack-dev-middleware": "^2.0.2", 92 | "@types/webpack-env": "^1.13.9", 93 | "@types/webpack-hot-middleware": "^2.16.5", 94 | "babel-loader": "^8.0.6", 95 | "babel-plugin-module-resolver": "^3.2.0", 96 | "circular-dependency-plugin": "^5.0.2", 97 | "cross-env": "^5.2.0", 98 | "css-loader": "^3.0.0", 99 | "cz-customizable": "^6.2.0", 100 | "file-loader": "^4.0.0", 101 | "fork-ts-checker-webpack-plugin": "^1.3.5", 102 | "glob": "^7.1.4", 103 | "husky": "^2.4.0", 104 | "identity-obj-proxy": "^3.0.0", 105 | "isomorphic-style-loader": "^5.1.0", 106 | "jest": "^24.8.0", 107 | "lint-staged": "^8.2.0", 108 | "mini-css-extract-plugin": "^0.7.0", 109 | "node-sass": "^4.12.0", 110 | "nodemon": "^1.19.1", 111 | "npm-run-all": "^4.1.5", 112 | "optimize-css-assets-webpack-plugin": "^5.0.1", 113 | "postcss-flexbugs-fixes": "^4.1.0", 114 | "postcss-loader": "^3.0.0", 115 | "postcss-preset-env": "^6.6.0", 116 | "prettier": "^1.18.2", 117 | "redux-devtools-extension": "2.13.8", 118 | "redux-mock-store": "^1.5.3", 119 | "resolve-url-loader": "^3.1.0", 120 | "sass-loader": "^7.1.0", 121 | "source-map-explorer": "^2.0.0", 122 | "style-loader": "^0.23.1", 123 | "terser-webpack-plugin": "^1.3.0", 124 | "ts-jest": "^24.0.2", 125 | "tslint": "^5.17.0", 126 | "tslint-react": "^4.0.0", 127 | "typescript": "^3.5.1", 128 | "webpack": "^4.33.0", 129 | "webpack-cli": "^3.3.4", 130 | "webpack-dev-middleware": "^3.7.0", 131 | "webpack-hot-middleware": "^2.25.0", 132 | "webpack-manifest-plugin": "^2.0.4", 133 | "webpack-merge": "^4.2.1", 134 | "webpack-node-externals": "^1.7.2", 135 | "webpackbar": "^3.2.0", 136 | "workbox-webpack-plugin": "^4.3.1" 137 | }, 138 | "config": { 139 | "commitizen": { 140 | "path": "node_modules/cz-customizable" 141 | } 142 | }, 143 | "husky": { 144 | "hooks": { 145 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 146 | "pre-commit": "lint-staged", 147 | "pre-push": "npm run test" 148 | } 149 | }, 150 | "lint-staged": { 151 | "*.{ts,tsx}": [ 152 | "tslint --fix", 153 | "prettier --write", 154 | "git add" 155 | ], 156 | "*.{js, json}": [ 157 | "prettier --write", 158 | "git add" 159 | ] 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/client/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { BrowserRouter } from 'react-router-dom'; 3 | import { hydrate, render } from 'react-dom'; 4 | import { Provider } from 'react-redux'; 5 | 6 | import { App } from '@core/app'; 7 | import { config } from '@core/common/config'; 8 | import { configureStore } from '@core/common/store'; 9 | 10 | import { registerServiceWorker } from './serviceWorker'; 11 | 12 | const initialState = window.__INITIAL_STATE__; 13 | const renderMethod = config.isDev && config.useRender ? render : hydrate; 14 | 15 | const store = configureStore(initialState); 16 | 17 | registerServiceWorker(); 18 | 19 | /** 20 | * To be able to reload the page 21 | * and see the latest code changes, 22 | * you must set the "localStorage.useRender" value in development mode 23 | */ 24 | renderMethod( 25 | 26 | 27 | 28 | 29 | , 30 | document.getElementById('root'), 31 | () => delete window.__INITIAL_STATE__ 32 | ); 33 | -------------------------------------------------------------------------------- /src/client/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | import { config } from '@core/common/config'; 2 | 3 | export function registerServiceWorker() { 4 | if (!config.isDev && config.isBrowser && 'serviceWorker' in navigator) { 5 | window.addEventListener('load', () => { 6 | navigator.serviceWorker.register('/service-worker.js'); 7 | }); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/core/api/index.ts: -------------------------------------------------------------------------------- 1 | import { RequestApi } from './request'; 2 | 3 | export interface AppApi { 4 | requestApi: RequestApi; 5 | } 6 | 7 | export const initializeApi = (fetchUrl: string): AppApi => ({ 8 | requestApi: new RequestApi(fetchUrl) 9 | }); 10 | -------------------------------------------------------------------------------- /src/core/api/request.ts: -------------------------------------------------------------------------------- 1 | export class RequestApi { 2 | constructor(private baseUrl: string, private defaultOptions?: RequestInit) {} 3 | 4 | public get(url: string, options?: RequestInit) { 5 | return this.createRequest('GET', url, options); 6 | } 7 | 8 | public post(url: string, body?: Object, options?: RequestInit) { 9 | const fullOptions = Object.assign({}, { body: JSON.stringify(body) }, options); 10 | return this.createRequest('POST', url, fullOptions); 11 | } 12 | 13 | public put(url: string, body?: Object, options?: RequestInit) { 14 | const fullOptions = Object.assign({}, { body: JSON.stringify(body) }, options); 15 | return this.createRequest('PUT', url, fullOptions); 16 | } 17 | 18 | public patch(url: string, body?: Object, options?: RequestInit) { 19 | const fullOptions = Object.assign({}, { body: JSON.stringify(body) }, options); 20 | return this.createRequest('PATCH', url, fullOptions); 21 | } 22 | 23 | public delete(url: string, body?: Object, options?: RequestInit) { 24 | const fullOptions = Object.assign({}, { body: JSON.stringify(body) }, options); 25 | return this.createRequest('DELETE', url, fullOptions); 26 | } 27 | 28 | private createRequest(method: string, url?: string, options?: RequestInit) { 29 | const fullOptions = Object.assign({ method }, this.defaultOptions, options); 30 | const fullUrl = `${this.baseUrl}${url}`; 31 | return fetch(fullUrl, fullOptions).then(res => { 32 | if (res.status !== 200) { 33 | throw Error(`Failed fetch to ${fullUrl}: ${res.status} ${res.statusText}`); 34 | } 35 | return res.json(); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/core/app.tsx: -------------------------------------------------------------------------------- 1 | import { hot } from 'react-hot-loader/root'; 2 | import * as React from 'react'; 3 | 4 | import { appRoutes } from './common/routes'; 5 | import { PageTemplate } from './ui/templates'; 6 | import { renderRouting } from './common/routing'; 7 | 8 | import './assets/styles/global.scss'; 9 | 10 | export const App = hot(() => { 11 | return {renderRouting(appRoutes)}; 12 | }); 13 | -------------------------------------------------------------------------------- /src/core/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/core/assets/styles/fonts.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afflicted-cat/react-typescript-express-ssr-example/9685f1190f976d5fc4988cf0f6240c5635949959/src/core/assets/styles/fonts.scss -------------------------------------------------------------------------------- /src/core/assets/styles/global.scss: -------------------------------------------------------------------------------- 1 | @import './variables'; 2 | @import './mixins'; 3 | @import './fonts'; 4 | 5 | html, 6 | body { 7 | margin: 0; 8 | padding: 0; 9 | font-size: 16px; 10 | line-height: 1.15; 11 | -webkit-text-size-adjust: 100%; 12 | } 13 | -------------------------------------------------------------------------------- /src/core/assets/styles/mixins.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afflicted-cat/react-typescript-express-ssr-example/9685f1190f976d5fc4988cf0f6240c5635949959/src/core/assets/styles/mixins.scss -------------------------------------------------------------------------------- /src/core/assets/styles/variables.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afflicted-cat/react-typescript-express-ssr-example/9685f1190f976d5fc4988cf0f6240c5635949959/src/core/assets/styles/variables.scss -------------------------------------------------------------------------------- /src/core/common/config.ts: -------------------------------------------------------------------------------- 1 | import { merge } from 'ramda'; 2 | 3 | export interface AppConfig { 4 | env: string; 5 | isDev: boolean; 6 | apiUrl: string; 7 | useRender: boolean; 8 | isBrowser: boolean; 9 | } 10 | 11 | type Environment = 'common' | 'development' | 'production'; 12 | 13 | type Config = { [key in Environment]: Partial }; 14 | 15 | // tslint:disable no-string-literal 16 | const isBrowser = process && process['browser']; 17 | const env = process.env.NODE_ENV || 'development'; 18 | 19 | const defaultConfig: Config = { 20 | common: { 21 | env, 22 | isBrowser, 23 | apiUrl: `https://api.github.com/`, 24 | isDev: process.env.NODE_ENV !== 'production', 25 | useRender: Boolean(isBrowser && localStorage.getItem('useRender')) 26 | }, 27 | development: {}, 28 | production: {} 29 | }; 30 | 31 | export const config: AppConfig = merge(defaultConfig.common, defaultConfig[env]); 32 | -------------------------------------------------------------------------------- /src/core/common/preload.ts: -------------------------------------------------------------------------------- 1 | import { ThunkAction } from 'redux-thunk'; 2 | import { AnyAction } from 'redux'; 3 | 4 | import { AppApi } from '@core/api'; 5 | 6 | import { AppState } from './reducer'; 7 | 8 | export type PreloadAction = (pathName?: string) => ThunkAction; 9 | 10 | // Actions that need to be dispatched at each route (SSR) 11 | export const preloadActions: PreloadAction[] = []; 12 | -------------------------------------------------------------------------------- /src/core/common/reducer.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import { commonReducer, CommonState } from '@core/features/common'; 4 | 5 | export interface AppState { 6 | common: CommonState; 7 | } 8 | 9 | export const reducer = combineReducers({ 10 | common: commonReducer 11 | }); 12 | -------------------------------------------------------------------------------- /src/core/common/routes.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { RouteConfig, RouteConfigComponentProps } from 'react-router-config'; 3 | import { unnest } from 'ramda'; 4 | 5 | import { PreloadAction } from './preload'; 6 | 7 | import { commonRoutes } from '@core/features/common'; 8 | 9 | export interface ExtraRoureProps { 10 | preloadActions?: PreloadAction | PreloadAction[]; 11 | } 12 | 13 | export interface RouteComponentProps extends RouteConfigComponentProps { 14 | routes?: AppRoute[]; 15 | } 16 | 17 | export interface AppRoute extends RouteConfig, ExtraRoureProps { 18 | routes?: AppRoute[]; 19 | component: React.ComponentType | React.ComponentType; 20 | } 21 | 22 | export const appRoutes = unnest([commonRoutes]) as AppRoute[]; 23 | -------------------------------------------------------------------------------- /src/core/common/routing.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Route, Switch } from 'react-router'; 3 | 4 | import { AppRoute } from './routes'; 5 | 6 | export function renderRouting(routes?: AppRoute[]) { 7 | if (!routes) return null; 8 | 9 | return ( 10 | 11 | {routes.map(props => ( 12 | 13 | ))} 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/core/common/store.ts: -------------------------------------------------------------------------------- 1 | import { composeWithDevTools } from 'redux-devtools-extension'; 2 | import { applyMiddleware, createStore, Store } from 'redux'; 3 | import thunk from 'redux-thunk'; 4 | 5 | import { initializeApi } from '@core/api'; 6 | 7 | import { reducer, AppState } from './reducer'; 8 | import { config } from './config'; 9 | 10 | export function configureStore(initialState?: AppState): Store { 11 | const api = initializeApi(config.apiUrl); 12 | 13 | let middleware = applyMiddleware(thunk.withExtraArgument(api)); 14 | 15 | if (config.isDev) { 16 | middleware = composeWithDevTools(middleware); 17 | } 18 | 19 | const store = createStore(reducer, initialState!, middleware); 20 | 21 | if (module.hot) { 22 | module.hot.accept('./reducer', () => { 23 | const { reducer: neextReducer } = require('./reducer'); 24 | store.replaceReducer(neextReducer); 25 | }); 26 | } 27 | 28 | return store as Store; 29 | } 30 | -------------------------------------------------------------------------------- /src/core/features/common/effects.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch } from 'redux'; 2 | 3 | import { AppApi } from '@core/api'; 4 | import { AppState } from '@core/common/reducer'; 5 | 6 | import { User } from './types'; 7 | import { errorFetchUser, fetchUser, successFetchUser } from './reducer'; 8 | 9 | export function getUser(name: string) { 10 | return async (dispatch: Dispatch, getState: () => AppState, { requestApi }: AppApi) => { 11 | try { 12 | dispatch(fetchUser()); 13 | const user: User = await requestApi.get(`users/${name}`); 14 | dispatch(successFetchUser({ user })); 15 | } catch ({ message }) { 16 | dispatch(errorFetchUser({ error: message })); 17 | } 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/core/features/common/index.ts: -------------------------------------------------------------------------------- 1 | export { commonReducer } from './reducer'; 2 | export { commonRoutes } from './routes'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/core/features/common/page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { createStructuredSelector } from 'reselect'; 3 | import { connect } from 'react-redux'; 4 | 5 | import { Align, Button, ErrorMessage, PreviewImage } from '@core/ui/atoms'; 6 | 7 | import { User } from './types'; 8 | import { getUser } from './effects'; 9 | import { userSelector, userFetchedSelector, userErrorSelector } from './selectors'; 10 | 11 | interface CommonPageProps { 12 | user?: User; 13 | error?: string; 14 | fetched: boolean; 15 | onClick: (name: string) => void; 16 | } 17 | 18 | const mapStateToProps = createStructuredSelector({ 19 | user: userSelector, 20 | fetched: userFetchedSelector, 21 | error: userErrorSelector 22 | }); 23 | 24 | const enhance = connect( 25 | mapStateToProps, 26 | { onClick: getUser } 27 | ); 28 | 29 | export function CommonPage({ user, fetched, onClick, error }: CommonPageProps) { 30 | return ( 31 | 32 | {user && ( 33 | 34 |

{user.login} avatar:

35 | 36 |
37 | )} 38 | 39 | 40 | 41 | 42 | {fetched && fetching...} 43 | {error && } 44 |
45 | ); 46 | } 47 | 48 | export const CommonPageContainer = enhance(CommonPage); 49 | -------------------------------------------------------------------------------- /src/core/features/common/reducer.ts: -------------------------------------------------------------------------------- 1 | import { createAction, handleActions } from 'redux-actions'; 2 | 3 | import { CommonState, User } from './types'; 4 | 5 | enum CommonActions { 6 | fetchUser = 'fetchUser', 7 | successFetchUser = 'successFetchUser', 8 | errorFetchUser = 'errorFetchUser' 9 | } 10 | 11 | interface Payload { 12 | user: User; 13 | error: string; 14 | } 15 | 16 | export const fetchUser = createAction(CommonActions.fetchUser); 17 | export const successFetchUser = createAction>(CommonActions.successFetchUser); 18 | export const errorFetchUser = createAction>(CommonActions.errorFetchUser); 19 | 20 | const initialState: CommonState = { 21 | fetched: false 22 | }; 23 | 24 | export const commonReducer = handleActions( 25 | { 26 | [CommonActions.fetchUser]: state => ({ ...state, fetched: true }), 27 | [CommonActions.successFetchUser]: (state, { payload }) => ({ fetched: false, user: payload!.user }), 28 | [CommonActions.errorFetchUser]: (state, { payload }) => ({ ...state, fetched: false, error: payload!.error }) 29 | }, 30 | initialState 31 | ); 32 | -------------------------------------------------------------------------------- /src/core/features/common/routes.ts: -------------------------------------------------------------------------------- 1 | import { AppRoute } from '@core/common/routes'; 2 | 3 | import { getUser } from './effects'; 4 | import { CommonPageContainer } from './page'; 5 | 6 | export const commonRoutes: AppRoute[] = [ 7 | { 8 | path: '/', 9 | exact: true, 10 | component: CommonPageContainer, 11 | preloadActions: () => getUser('weyheyhey') 12 | } 13 | ]; 14 | -------------------------------------------------------------------------------- /src/core/features/common/selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | import { AppState } from '@core/common/reducer'; 4 | 5 | export const commonRootSelector = (state: AppState) => state.common; 6 | 7 | export const userSelector = createSelector( 8 | commonRootSelector, 9 | common => common.user 10 | ); 11 | export const userFetchedSelector = createSelector( 12 | commonRootSelector, 13 | common => common.fetched 14 | ); 15 | export const userErrorSelector = createSelector( 16 | commonRootSelector, 17 | common => common.error 18 | ); 19 | -------------------------------------------------------------------------------- /src/core/features/common/types.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: number; 3 | url: string; 4 | login: string; 5 | avatar_url: string; 6 | } 7 | 8 | export interface CommonState { 9 | fetched: boolean; 10 | user?: User; 11 | error?: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/core/ui/atoms/Align/Align.scss: -------------------------------------------------------------------------------- 1 | .align { 2 | width: 100%; 3 | display: flex; 4 | flex-wrap: wrap; 5 | &.v-top { 6 | align-items: flex-start; 7 | } 8 | &.v-center { 9 | align-items: center; 10 | } 11 | &.v-bottom { 12 | align-items: flex-end; 13 | } 14 | &.v-stretch { 15 | align-items: stretch; 16 | } 17 | &.h-start { 18 | justify-content: flex-start; 19 | } 20 | &.h-center { 21 | justify-content: center; 22 | } 23 | &.h-end { 24 | justify-content: flex-end; 25 | } 26 | &.h-between { 27 | justify-content: space-between; 28 | } 29 | &.h-around { 30 | justify-content: space-around; 31 | } 32 | &.h-evenly { 33 | justify-content: space-evenly; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/core/ui/atoms/Align/Align.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | import styles from './Align.scss'; 5 | 6 | interface Props { 7 | children: React.ReactNode; 8 | className?: 'string'; 9 | vertical?: 'top' | 'center' | 'bottom' | 'stretch'; 10 | horizontal?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly'; 11 | } 12 | 13 | export function Align({ vertical = 'center', horizontal = 'center', className, children }: Props) { 14 | return ( 15 |
16 | {children} 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/core/ui/atoms/Align/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Align'; 2 | -------------------------------------------------------------------------------- /src/core/ui/atoms/Button/Button.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | cursor: pointer; 3 | display: inline-block; 4 | min-height: 1em; 5 | outline: 0; 6 | border: none; 7 | vertical-align: baseline; 8 | background: #e0e1e2 none; 9 | color: rgba(0, 0, 0, 0.6); 10 | margin: 0.5rem 0.25rem; 11 | padding: 0.78571429em 1.5em 0.78571429em; 12 | text-transform: none; 13 | text-shadow: none; 14 | font-weight: 700; 15 | line-height: 1em; 16 | font-style: normal; 17 | text-align: center; 18 | text-decoration: none; 19 | border-radius: 0.28571429rem; 20 | box-shadow: 0 0 0 1px transparent inset, 0 0 0 0 rgba(34, 36, 38, 0.15) inset; 21 | user-select: none; 22 | transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, box-shadow 0.1s ease, background 0.1s ease; 23 | &:hover { 24 | background-color: #cacbcd; 25 | box-shadow: 0 0 0 1px transparent inset, 0 0 0 0 rgba(34, 36, 38, 0.15) inset; 26 | color: rgba(0, 0, 0, 0.8); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/core/ui/atoms/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | import styles from './Button.scss'; 5 | 6 | interface Props { 7 | text?: string; 8 | className?: string; 9 | children?: React.ReactNode; 10 | onClick?: (e: React.SyntheticEvent) => void; 11 | } 12 | 13 | export function Button({ onClick, text, children, className }: Props) { 14 | return ( 15 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/core/ui/atoms/Button/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Button'; 2 | -------------------------------------------------------------------------------- /src/core/ui/atoms/ErrorMessage/ErrorMessage.scss: -------------------------------------------------------------------------------- 1 | .errorMessage { 2 | color: red; 3 | font-style: italic; 4 | font-size: 0.875rem; 5 | padding: 0.5rem; 6 | text-align: center; 7 | } 8 | -------------------------------------------------------------------------------- /src/core/ui/atoms/ErrorMessage/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | import styles from './ErrorMessage.scss'; 5 | 6 | interface Props { 7 | message: string; 8 | className?: string; 9 | } 10 | 11 | export function ErrorMessage({ message, className }: Props) { 12 | return
{message}
; 13 | } 14 | -------------------------------------------------------------------------------- /src/core/ui/atoms/ErrorMessage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ErrorMessage'; 2 | -------------------------------------------------------------------------------- /src/core/ui/atoms/Logo/Logo.scss: -------------------------------------------------------------------------------- 1 | .logo { 2 | animation: app-logo-spin infinite 20s linear; 3 | height: 85px; 4 | box-sizing: border-box; 5 | } 6 | 7 | @keyframes app-logo-spin { 8 | from { 9 | transform: rotate(0deg); 10 | } 11 | to { 12 | transform: rotate(360deg); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/core/ui/atoms/Logo/Logo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import image from '@core/assets/images/logo.svg'; 4 | 5 | import styles from './Logo.scss'; 6 | 7 | export function Logo() { 8 | return logo; 9 | } 10 | -------------------------------------------------------------------------------- /src/core/ui/atoms/Logo/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Logo'; 2 | -------------------------------------------------------------------------------- /src/core/ui/atoms/PreviewImage/PreviewImage.scss: -------------------------------------------------------------------------------- 1 | .previewImage { 2 | width: 200px; 3 | margin: 0 auto 0.625rem; 4 | } 5 | -------------------------------------------------------------------------------- /src/core/ui/atoms/PreviewImage/PreviewImage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | import styles from './PreviewImage.scss'; 5 | 6 | interface Props { 7 | src: string; 8 | className?: string; 9 | } 10 | 11 | export function PreviewImage({ src, className }: Props) { 12 | return ; 13 | } 14 | -------------------------------------------------------------------------------- /src/core/ui/atoms/PreviewImage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PreviewImage'; 2 | -------------------------------------------------------------------------------- /src/core/ui/atoms/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Button'; 2 | export * from './Align'; 3 | export * from './Logo'; 4 | export * from './ErrorMessage'; 5 | export * from './PreviewImage'; 6 | -------------------------------------------------------------------------------- /src/core/ui/molecules/Header/Header.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | background-color: #222; 3 | padding: 20px; 4 | color: white; 5 | text-align: center; 6 | box-sizing: border-box; 7 | width: 100%; 8 | & .title { 9 | font-size: 1.5em; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/core/ui/molecules/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | import { Logo } from '@core/ui/atoms'; 5 | 6 | import styles from './Header.scss'; 7 | 8 | interface Props { 9 | title: string; 10 | className?: string; 11 | } 12 | 13 | export function Header({ title, className }: Props) { 14 | return ( 15 |
16 | 17 |

{title}

18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/core/ui/molecules/Header/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Header'; 2 | -------------------------------------------------------------------------------- /src/core/ui/molecules/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Header'; 2 | -------------------------------------------------------------------------------- /src/core/ui/templates/PageTemplate/PageTemplate.scss: -------------------------------------------------------------------------------- 1 | .pageTemplate { 2 | width: 100%; 3 | min-height: 100vh; 4 | text-align: center; 5 | } 6 | -------------------------------------------------------------------------------- /src/core/ui/templates/PageTemplate/PageTemplate.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { Header } from '@core/ui/molecules'; 4 | 5 | import styles from './PageTemplate.scss'; 6 | 7 | interface Props { 8 | title: string; 9 | children: React.ReactNode; 10 | } 11 | 12 | export function PageTemplate({ title, children }: Props) { 13 | return ( 14 |
15 |
16 | {children} 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/core/ui/templates/PageTemplate/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PageTemplate'; 2 | -------------------------------------------------------------------------------- /src/core/ui/templates/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PageTemplate'; 2 | -------------------------------------------------------------------------------- /src/server/exitHandler.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable: no-any 2 | const exitHandler = (options: any) => (err: any) => { 3 | if (err && err.stack) { 4 | process.stdout.write(err.stack); 5 | } 6 | if (options.exit) { 7 | process.exit(1); 8 | } 9 | }; 10 | 11 | process.stdin.resume(); 12 | 13 | // catches ctrl+c event 14 | process.on('SIGINT', exitHandler({ exit: true })); 15 | 16 | // catches "kill pid" (for example: nodemon restart) 17 | process.on('SIGUSR1', exitHandler({ exit: true })); 18 | process.on('SIGUSR2', exitHandler({ exit: true })); 19 | 20 | // catches uncaught exceptions 21 | process.on('uncaughtException', exitHandler({ exit: true })); 22 | process.on('unhandledRejection', exitHandler({ exit: false })); 23 | -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | import webpackDevMiddleware from 'webpack-dev-middleware'; 2 | import webpackHotMiddleware from 'webpack-hot-middleware'; 3 | import webpack from 'webpack'; 4 | import express from 'express'; 5 | import helmet from 'helmet'; 6 | import path from 'path'; 7 | 8 | import 'isomorphic-fetch'; 9 | import './exitHandler'; 10 | 11 | import { assetsParser } from './middlewares/assetsParser'; 12 | import { getRequire } from './lib/utils'; 13 | import { router } from './router'; 14 | 15 | // tslint:disable:no-console 16 | 17 | const isProduction = process.env.NODE_ENV === 'production'; 18 | const host = process.env.HOST || 'localhost'; 19 | const port = process.env.PORT || 3001; 20 | const app = express(); 21 | 22 | app.disable('x-powered-by'); 23 | 24 | app.use(helmet()); 25 | 26 | if (isProduction) { 27 | // In real app better to use nginx for static assets 28 | const httpHeaders = { maxAge: 31536000, redirect: false, lastModified: true }; 29 | app.use(express.static(path.resolve(process.cwd(), 'dist'), httpHeaders)); 30 | } 31 | 32 | if (!isProduction) { 33 | const webpackConfig = getRequire()(path.resolve(process.cwd(), 'webpack.config')); 34 | const compiler = webpack(webpackConfig); 35 | app.use( 36 | webpackDevMiddleware(compiler, { 37 | publicPath: webpackConfig.output.publicPath, 38 | serverSideRender: true, 39 | stats: 'errors-only', 40 | logLevel: 'error' 41 | }) 42 | ); 43 | app.use(webpackHotMiddleware(compiler, { log: console.log })); 44 | } 45 | 46 | app.use(assetsParser(isProduction)); 47 | app.use('/', router); 48 | 49 | app.use((err: string, req: express.Request, res: express.Response, next: express.NextFunction) => { 50 | if (!isProduction) { 51 | return res.status(500).send(err); 52 | } 53 | 54 | return res.sendStatus(500); 55 | }); 56 | 57 | app.listen(port, () => { 58 | console.info(`✅✅✅ Server is running at http://${host}:${port} ✅✅✅`); 59 | }); 60 | -------------------------------------------------------------------------------- /src/server/lib/render/index.ts: -------------------------------------------------------------------------------- 1 | export * from './renderHtml'; 2 | export * from './renderApp'; 3 | -------------------------------------------------------------------------------- /src/server/lib/render/renderApp.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { renderToString } from 'react-dom/server'; 3 | import { StaticRouter } from 'react-router'; 4 | import { Provider } from 'react-redux'; 5 | import { Store } from 'redux'; 6 | 7 | import { AppState } from '@core/common/reducer'; 8 | import { App } from '@core/app'; 9 | 10 | export const renderApp = (store: Store, context?: object, location?: string | object) => { 11 | const appRoot = ( 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | 19 | return renderToString(appRoot); 20 | }; 21 | -------------------------------------------------------------------------------- /src/server/lib/render/renderHtml.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { renderToStaticMarkup } from 'react-dom/server'; 3 | import Helmet from 'react-helmet'; 4 | 5 | import { config } from '@core/common/config'; 6 | 7 | interface HtmlProps { 8 | content: string; 9 | styles?: string[]; 10 | scripts?: string[]; 11 | initialValues?: string; 12 | } 13 | 14 | export const renderHtml = ({ content, styles = [], scripts = [], initialValues = '' }: HtmlProps) => { 15 | const helmet = Helmet.renderStatic(); 16 | const htmlAttrs = helmet.htmlAttributes.toComponent(); 17 | const bodyAttrs = helmet.bodyAttributes.toComponent(); 18 | 19 | const html = ( 20 | 21 | 22 | 23 | 24 | {helmet.title.toComponent()} 25 | {helmet.meta.toComponent()} 26 | {helmet.link.toComponent()} 27 | {styles.map(href => ( 28 | 29 | ))} 30 | {scripts.map(src => ( 31 | 32 | ))} 33 | {styles.map(href => ( 34 | 35 | ))} 36 | 37 |