├── example.gif ├── .babelrc ├── webpack_config ├── client │ ├── webpack.prod.babel.js │ ├── webpack.dev.babel.js │ └── webpack.base.babel.js ├── server │ ├── webpack.dev.babel.js │ ├── webpack.prod.babel.js │ └── webpack.base.babel.js ├── isomorphic.config.js ├── config.js └── devServer.js ├── src ├── common │ └── index.jsx ├── client │ └── index.jsx └── server │ ├── middlewares │ └── index.js │ ├── api │ └── index.js │ ├── ssr │ ├── index.js │ └── Html.jsx │ ├── decorator.js │ └── index.js ├── .travis.yml ├── .eslintrc.json ├── .vcmrc ├── tests └── index.test.js ├── .gitignore ├── package.json ├── CHANGELOG.md ├── README.md └── LICENSE /example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metnew/tiny-universal-skeleton/HEAD/example.gif -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["react-hot-loader/babel"], 3 | "presets": ["env", "react"] 4 | } 5 | -------------------------------------------------------------------------------- /webpack_config/client/webpack.prod.babel.js: -------------------------------------------------------------------------------- 1 | import base from './webpack.base.babel' 2 | 3 | export default base 4 | -------------------------------------------------------------------------------- /webpack_config/server/webpack.dev.babel.js: -------------------------------------------------------------------------------- 1 | import baseWebpackConfig from './webpack.base.babel' 2 | 3 | export default baseWebpackConfig 4 | -------------------------------------------------------------------------------- /webpack_config/server/webpack.prod.babel.js: -------------------------------------------------------------------------------- 1 | import baseWebpackConfig from './webpack.base.babel' 2 | 3 | export default baseWebpackConfig 4 | -------------------------------------------------------------------------------- /src/common/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Root = () => { 4 | return ( 5 |

Hello world!

6 | ) 7 | } 8 | 9 | export default Root 10 | -------------------------------------------------------------------------------- /src/client/index.jsx: -------------------------------------------------------------------------------- 1 | // Application 2 | import React from 'react' 3 | import {hydrate} from 'react-dom' 4 | import Root from 'common' 5 | 6 | hydrate(, document.getElementById('app')) 7 | 8 | if (module.hot) { 9 | module.hot.accept() 10 | } 11 | -------------------------------------------------------------------------------- /src/server/middlewares/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import morgan from 'morgan' 3 | 4 | export default (app) => { 5 | app.use(morgan('dev')) 6 | app.use( 7 | express.static(process.env.CLIENT_DIST_PATH, { 8 | index: false 9 | }) 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/server/api/index.js: -------------------------------------------------------------------------------- 1 | import {Router} from 'express' 2 | import chalk from 'chalk' 3 | const router = Router() 4 | 5 | router.get('/hello', (req, res) => { 6 | console.log(chalk.red`You find a secret location!`) 7 | res.send('You find a secret route!') 8 | }) 9 | 10 | export default router 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - node_modules 5 | notifications: 6 | email: false 7 | node_js: 8 | - '6' 9 | - '8' 10 | install: 11 | # - npm install -g codecov 12 | - npm install 13 | script: 14 | - npm run test 15 | # after_script: 16 | # - codecov 17 | -------------------------------------------------------------------------------- /src/server/ssr/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {renderToString} from 'react-dom/server' 3 | import Html from './Html' 4 | import Root from 'common' 5 | import fs from 'fs' 6 | 7 | export default (req, res, next) => { 8 | const App = renderToString() 9 | const assets = fs.readFileSync(`webpack-assets`, 'utf8') 10 | const html = Html({App, assets: JSON.parse(assets)}) 11 | res.send(html) 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:react/recommended", "standard"], 3 | "parser": "babel-eslint", 4 | "plugins": ["react", "babel"], 5 | "parserOptions": { 6 | "ecmaFeatures": { 7 | "jsx": true, 8 | "modules": true 9 | } 10 | }, 11 | "env": { 12 | "browser": true, 13 | "amd": true, 14 | "es6": true, 15 | "node": true, 16 | "jest": true 17 | }, 18 | "rules": { 19 | "no-tabs": 0, 20 | "indent": [2, "tab"] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.vcmrc: -------------------------------------------------------------------------------- 1 | { 2 | "types": ["feat", "fix", "docs", "style", "refactor", "perf", "test", "build", "ci", "chore", "revert"], 3 | "scope": { 4 | "required": false, 5 | "allowed": ["*"], 6 | "validate": false, 7 | "multiple": false 8 | }, 9 | "warnOnFail": false, 10 | "maxSubjectLength": 100, 11 | "subjectPattern": ".+", 12 | "subjectPatternErrorMsg": "subject does not match subject pattern!", 13 | "helpMessage": "", 14 | "autoFix": false 15 | } 16 | -------------------------------------------------------------------------------- /src/server/decorator.js: -------------------------------------------------------------------------------- 1 | import addMiddlewares from './middlewares' 2 | import API from './api' 3 | import SSR from './ssr' 4 | 5 | /** 6 | * Mount API, SSR and middlewares to app. 7 | * @param {Object} app - Express server instance 8 | * @return {Object} - Decorated server instance 9 | */ 10 | export default function (app) { 11 | // Add global middlewares 12 | addMiddlewares(app) 13 | // Add API 14 | app.use('/api/v1', API) 15 | // Add SSR 16 | app.use(SSR) 17 | return app 18 | } 19 | -------------------------------------------------------------------------------- /webpack_config/client/webpack.dev.babel.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack' 2 | import baseWebpackConfig from './webpack.base.babel' 3 | import WriteFilePlugin from 'write-file-webpack-plugin' 4 | 5 | baseWebpackConfig.entry.client = [ 6 | 'react-hot-loader/patch', 7 | 'webpack-hot-middleware/client?reload=true', 8 | baseWebpackConfig.entry.client 9 | ] 10 | 11 | // add dev plugins 12 | baseWebpackConfig.plugins.push( 13 | new webpack.HotModuleReplacementPlugin(), 14 | new WriteFilePlugin() 15 | ) 16 | 17 | export default baseWebpackConfig 18 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | const str1 = `Do you search for the awesomest boilerplate with HMR/Tests/Redux/?` 3 | const link = 'https://github.com/Metnew/react-semantic.ui-starter' 4 | const str2 = `Try it! ` 5 | 6 | const chaslkStr1 = chalk.green(str1) 7 | const chalkStr2 = chalk.yellow(str2) 8 | describe(`Where are tests?`, () => { 9 | it(`You can find tests for the same but more advanced setup: ${link}`, () => { 10 | console.log(chaslkStr1 + '\n' + chalkStr2 + chalk.underline.red(link)) 11 | expect(true).toEqual(true) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import chalk from 'chalk' 3 | 4 | const app = express() 5 | const PORT = 3000 6 | const isProduction = process.env.NODE_ENV === 'production' 7 | const pathToServerDecorator = isProduction 8 | ? './decorator' 9 | : '../../webpack_config/devServer' 10 | // NOTE: Such dynamic imports is a bad practice! 11 | // It's used here to show that our `serverDecorator` is a dynamic thing. 12 | const serverDecorator = require(pathToServerDecorator).default 13 | serverDecorator(app) 14 | 15 | app.listen(PORT, () => { 16 | console.log(chalk.green(`SERVER IS LISTENING ON ${PORT}`)) 17 | }) 18 | -------------------------------------------------------------------------------- /webpack_config/isomorphic.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | */ 4 | import path from 'path' 5 | import config from './config' 6 | import webpack from 'webpack' 7 | 8 | const { 9 | srcPath, 10 | rootPath, 11 | NODE_ENV 12 | } = config 13 | 14 | const definePluginArgs = { 15 | 'process.env.NODE_ENV': JSON.stringify(NODE_ENV) 16 | } 17 | 18 | export default { 19 | resolve: { 20 | extensions: ['.js', '.json', '.jsx'], 21 | modules: [srcPath, path.join(rootPath, 'node_modules')] 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.(js|jsx)$/, 27 | enforce: 'pre', 28 | use: [ 29 | { 30 | loader: 'eslint-loader', 31 | options: { 32 | fix: true 33 | } 34 | } 35 | ], 36 | exclude: [/node_modules/] 37 | }, 38 | { 39 | test: /\.(js|jsx)$/, 40 | use: 'babel-loader', 41 | exclude: [/node_modules/] 42 | } 43 | ] 44 | }, 45 | plugins: [ 46 | new webpack.NamedModulesPlugin(), 47 | new webpack.DefinePlugin(definePluginArgs) 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /webpack_config/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file for config stuff that's used for webpack configuration, but isn't passed to webpack compiler 3 | */ 4 | 5 | import path from 'path' 6 | 7 | // Paths 8 | const rootPath = path.join(__dirname, '../') // = "/" 9 | const distPath = path.join(rootPath, './dist') // = "/dist" 10 | const srcPath = path.join(rootPath, './src') // = "/src" 11 | const srcCommonPath = path.join(srcPath, './common') // = "/src/common" 12 | 13 | const NODE_ENV = process.env.NODE_ENV || 'development' 14 | const CLIENT_DIST_PATH = path.join(distPath, './client') // = "/dist/client" 15 | 16 | // compute isProduction based on NODE_ENV 17 | const isProduction = NODE_ENV === 'production' 18 | 19 | export default { 20 | title: 'Tiny-universal-skeleton', 21 | publicPath: '/', 22 | // i18n object 23 | isProduction, 24 | // Env vars 25 | NODE_ENV, 26 | CLIENT_DIST_PATH, 27 | // It's better to define pathes in one file 28 | // and then use everywhere across app 29 | srcPath, 30 | srcCommonPath, 31 | distPath, 32 | rootPath 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | 61 | dist 62 | -------------------------------------------------------------------------------- /src/server/ssr/Html.jsx: -------------------------------------------------------------------------------- 1 | const IndexHTMLComponent = ({App, assets}) => { 2 | const createBody = () => { 3 | const html = ` 4 |
${App}
5 | ${Object.keys(assets) 6 | .filter(bundleName => assets[bundleName].js) 7 | .map(bundleName => { 8 | const path = assets[bundleName].js 9 | return `` 10 | }) 11 | .join('')}` 12 | return html 13 | } 14 | 15 | const createHead = () => { 16 | const html = ` 17 | 18 | Tiny-universal-skeleton 19 | 20 | 24 | 25 | 26 | ` 27 | return html 28 | } 29 | 30 | const itIsUniversal = false 31 | const hello = itIsUniversal ? '

Hello from hot-reloaded server!

' : '' 32 | 33 | return ` 34 | ${createHead()} 35 | 36 | ${hello} 37 | ${createBody()} 38 | 39 | ` 40 | } 41 | 42 | export default IndexHTMLComponent 43 | -------------------------------------------------------------------------------- /webpack_config/client/webpack.base.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import childProcess from 'child_process' 3 | import webpack from 'webpack' 4 | import config from '../config' 5 | import isomorphicWebpackConfig from '../isomorphic.config' 6 | import AssetsPlugin from 'assets-webpack-plugin' 7 | const { 8 | CLIENT_DIST_PATH, 9 | NODE_ENV, 10 | srcPath, 11 | publicPath 12 | } = config 13 | 14 | const exec = childProcess.execSync 15 | exec(`rm -rf ${CLIENT_DIST_PATH}`) 16 | 17 | const definePluginArgs = { 18 | 'process.env.NODE_ENV': JSON.stringify(NODE_ENV), 19 | 'process.env.BROWSER': JSON.stringify(true) 20 | } 21 | 22 | const baseBuild = { 23 | name: 'client', 24 | target: 'web', 25 | entry: { 26 | client: path.join(srcPath, './client') 27 | }, 28 | output: { 29 | filename: '[name].js', 30 | publicPath, 31 | path: CLIENT_DIST_PATH, 32 | chunkFilename: '[name].[chunkhash:6].js' 33 | }, 34 | performance: { 35 | hints: false 36 | }, 37 | module: { 38 | rules: isomorphicWebpackConfig.module.rules 39 | }, 40 | resolve: { 41 | modules: isomorphicWebpackConfig.resolve.modules, 42 | extensions: isomorphicWebpackConfig.resolve.extensions.concat() 43 | }, 44 | plugins: isomorphicWebpackConfig.plugins.concat([ 45 | new webpack.DefinePlugin(definePluginArgs), 46 | new AssetsPlugin({ 47 | path: CLIENT_DIST_PATH 48 | }) 49 | ]) 50 | } 51 | 52 | export default baseBuild 53 | -------------------------------------------------------------------------------- /webpack_config/server/webpack.base.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs' 3 | import webpack from 'webpack' 4 | import config from '../config' 5 | import isomorphicWebpackConfig from '../isomorphic.config' 6 | import childProcess from 'child_process' 7 | 8 | // Cleare dist dir before run 9 | const exec = childProcess.execSync 10 | exec(`rm -rf ${config.distPath}/server`) 11 | 12 | const definePluginArgs = { 13 | 'process.env.BROWSER': JSON.stringify(false), 14 | 'process.env.NODE_ENV': JSON.stringify(config.NODE_ENV), 15 | 'process.env.CLIENT_DIST_PATH': JSON.stringify(config.CLIENT_DIST_PATH) 16 | } 17 | 18 | const devtool = config.isProduction ? 'cheap-source-map' : 'source-map' 19 | const entry = config.isProduction 20 | ? path.join(config.srcPath, './server') 21 | : path.join(config.srcPath, './server/decorator') 22 | 23 | let nodeModules = {} 24 | fs 25 | .readdirSync('node_modules') 26 | .filter(x => { 27 | return ['.bin'].indexOf(x) === -1 28 | }) 29 | .forEach(mod => { 30 | nodeModules[mod] = 'commonjs ' + mod 31 | }) 32 | 33 | const baseWebpackConfig = { 34 | name: 'server', 35 | entry, 36 | target: 'node', 37 | devtool, 38 | output: { 39 | path: path.join(config.distPath, './server'), 40 | filename: 'index.js', 41 | libraryTarget: 'commonjs2' 42 | }, 43 | externals: nodeModules, 44 | performance: { 45 | hints: false 46 | }, 47 | resolve: { 48 | extensions: isomorphicWebpackConfig.resolve.extensions, 49 | modules: isomorphicWebpackConfig.resolve.modules, 50 | alias: { 51 | 'webpack-assets': `${config.CLIENT_DIST_PATH}/webpack-assets.json` 52 | } 53 | }, 54 | module: { 55 | rules: isomorphicWebpackConfig.module.rules 56 | }, 57 | plugins: isomorphicWebpackConfig.plugins.concat([ 58 | new webpack.DefinePlugin(definePluginArgs) 59 | ]), 60 | node: { 61 | __dirname: true, 62 | global: true 63 | } 64 | } 65 | 66 | export default baseWebpackConfig 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tiny-universal-skeleton", 3 | "version": "1.3.0", 4 | "main": "src/server/index.js", 5 | "description": "Tiny Universal Skeleton", 6 | "license": "Apache-2.0", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/Metnew/tiny-universal-skeleton.git" 10 | }, 11 | "dependencies": { 12 | "chalk": "^2.3.0", 13 | "express": "^4.16.2", 14 | "lodash": "^4.17.4", 15 | "morgan": "^1.9.0", 16 | "react": "^16.0.0", 17 | "react-dom": "^16.0.0" 18 | }, 19 | "devDependencies": { 20 | "assets-webpack-plugin": "^3.5.1", 21 | "babel-cli": "^6.26.0", 22 | "babel-core": "^6.26.0", 23 | "babel-eslint": "^8.0.1", 24 | "babel-loader": "^7.1.2", 25 | "babel-preset-env": "^1.6.1", 26 | "babel-preset-react": "^6.24.1", 27 | "babel-register": "6.26.0", 28 | "babel-runtime": "^6.26.0", 29 | "commitizen": "^2.9.6", 30 | "eslint": "^4.10.0", 31 | "eslint-config-standard": "^10.2.1", 32 | "eslint-loader": "^1.9.0", 33 | "eslint-plugin-babel": "^4.1.2", 34 | "eslint-plugin-import": "^2.8.0", 35 | "eslint-plugin-node": "^5.2.1", 36 | "eslint-plugin-promise": "^3.6.0", 37 | "eslint-plugin-react": "^7.4.0", 38 | "eslint-plugin-standard": "^3.0.1", 39 | "husky": "^0.14.3", 40 | "jest": "^21.2.1", 41 | "react-hot-loader": "^3.1.1", 42 | "standard-version": "^4.2.0", 43 | "validate-commit-msg": "^2.14.0", 44 | "webpack": "^3.8.1", 45 | "webpack-dev-middleware": "^1.12.0", 46 | "webpack-get-code-on-done": "^1.0.11", 47 | "webpack-hot-middleware": "^2.20.0", 48 | "write-file-webpack-plugin": "^4.2.0" 49 | }, 50 | "scripts": { 51 | "test": "jest", 52 | "cz": "git-cz", 53 | "commitmsg": "validate-commit-msg", 54 | "build": "npm run frontend_build && npm run server_build", 55 | "server_build": "NODE_ENV=production webpack --config webpack_config/server/webpack.prod.babel.js", 56 | "frontend_build": "NODE_ENV=production webpack --config webpack_config/client/webpack.prod.babel.js", 57 | "start": "NODE_ENV=development babel-node ./src/server/index", 58 | "release": "standard-version" 59 | }, 60 | "engines": { 61 | "node": ">=6" 62 | }, 63 | "author": { 64 | "name": "Vladimir Metnew", 65 | "email": "vladimirmetnew@gmail.com" 66 | }, 67 | "keywords": [ 68 | "example", 69 | "hmr", 70 | "react", 71 | "hot-reloading", 72 | "universal", 73 | "ssr", 74 | "server-side rendering", 75 | "hot" 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /webpack_config/devServer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | */ 4 | import webpack from 'webpack' 5 | import webpackDevMiddleware from 'webpack-dev-middleware' 6 | import webpackHotMiddleware from 'webpack-hot-middleware' 7 | import webpackGetCodeOnDone from 'webpack-get-code-on-done' 8 | import client from './client/webpack.dev.babel' 9 | import server from './server/webpack.dev.babel' 10 | // Configs for MultiCompiler 11 | const webpackConfig = [client, server] 12 | // Get MultiCompiler 13 | const compiler = webpack(webpackConfig) 14 | // Create devMiddleWare 15 | const devMiddleWare = webpackDevMiddleware(compiler, { 16 | serverRenderer: true, 17 | publicPath: webpackConfig[0].output.publicPath, 18 | quiet: false, 19 | noInfo: true 20 | }) 21 | 22 | // NOTE: Every time we apply our compiled code to development server 23 | // We add new middlewares from new code, but don't remove old middlewares from old code 24 | // Number of middlewares that Express app should have out-of-box 25 | let prevSize = null 26 | /** 27 | * @desc Adds dev middlewares + your code to an express server instance 28 | * @param {ExpressServer} app - Express dev server to which compiled code will be applied 29 | */ 30 | export default function (app) { 31 | /** 32 | * @desc Function that executes after your server-side code compiles 33 | * @param {Function} serverSideCode - compiled server-side code 34 | */ 35 | const done = serverSideCode => { 36 | // Get current stack of the app (e.g. applied to Express server middlewares) 37 | const {stack} = app._router 38 | // get current lenght of stack 39 | const {length} = stack 40 | // When we run server first time we don't have any applied middlewares from compiled code 41 | prevSize = prevSize || length 42 | if (length > prevSize) { 43 | // TL;DR: Remove already compiled code 44 | // That means that we already applied our code to devServer 45 | // And we can remove already applied middlewares from the last compilation 46 | app._router.stack = stack.slice(0, prevSize) 47 | } 48 | // Apply newly compiled code to our app 49 | serverSideCode(app) 50 | } 51 | 52 | // webpack Compiler for server 53 | const serverCompiler = compiler.compilers.find( 54 | compiler => compiler.name === 'server' 55 | ) 56 | // webpack Compiler for client 57 | const clientCompiler = compiler.compilers.find( 58 | compiler => compiler.name === 'client' 59 | ) 60 | // Add webpack-dev-middleware 61 | app.use(devMiddleWare) 62 | // Add webpack-hot-middleware 63 | app.use( 64 | webpackHotMiddleware(clientCompiler, { 65 | log: console.log 66 | }) 67 | ) 68 | // Run `done` after serverCompiler emits `done` with a newly compiled code as argument 69 | webpackGetCodeOnDone(serverCompiler, done) 70 | } 71 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | # [1.3.0](https://github.com/Metnew/tiny-universal-skeleton/compare/v1.2.0...v1.3.0) (2017-11-04) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * **client:** adapt to R16 ([b440c71](https://github.com/Metnew/tiny-universal-skeleton/commit/b440c71)) 12 | * **package.json:** remove unused deps ([569c1b2](https://github.com/Metnew/tiny-universal-skeleton/commit/569c1b2)) 13 | * **webpack_config:** fix eslint issues ([c310f19](https://github.com/Metnew/tiny-universal-skeleton/commit/c310f19)) 14 | 15 | 16 | ### Features 17 | 18 | * **.babelrc:** use babel-preset-env, remove unused plugins and presets ([ca3bfd4](https://github.com/Metnew/tiny-universal-skeleton/commit/ca3bfd4)) 19 | 20 | 21 | 22 | 23 | # [1.2.0](https://github.com/Metnew/tiny-universal-skeleton/compare/v1.1.0...v1.2.0) (2017-09-19) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * **.eslintrc:** remove prefer-const rule ([33e63b6](https://github.com/Metnew/tiny-universal-skeleton/commit/33e63b6)) 29 | * **.eslintrc:** remove unused globals ([942ad7f](https://github.com/Metnew/tiny-universal-skeleton/commit/942ad7f)) 30 | * fix eslint errors ([c529c72](https://github.com/Metnew/tiny-universal-skeleton/commit/c529c72)) 31 | * remove webpack-assets-manifest from the project ([8151e5e](https://github.com/Metnew/tiny-universal-skeleton/commit/8151e5e)) 32 | 33 | 34 | ### Features 35 | 36 | * **package.json:** update deps versions ([2ec1e3a](https://github.com/Metnew/tiny-universal-skeleton/commit/2ec1e3a)) 37 | 38 | 39 | 40 | 41 | # [1.1.0](https://github.com/Metnew/tiny-universal-skeleton/compare/v1.0.0...v1.1.0) (2017-09-12) 42 | 43 | 44 | 45 | 46 | # [1.0.0](https://github.com/Metnew/tiny-universal-skeleton/compare/v0.0.0...v1.0.0) (2017-09-12) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * **server:** fix path to server decorator, fix import of server decorator ([b18997e](https://github.com/Metnew/tiny-universal-skeleton/commit/b18997e)) 52 | * **server:** remove flowtypes ([f389c87](https://github.com/Metnew/tiny-universal-skeleton/commit/f389c87)) 53 | * **server/decorator:** remove flowtypes ([411f421](https://github.com/Metnew/tiny-universal-skeleton/commit/411f421)) 54 | * **webpack_config:** rename to devServer, fix import path of client and server webpack configs ([bce1f3a](https://github.com/Metnew/tiny-universal-skeleton/commit/bce1f3a)) 55 | 56 | 57 | ### Features 58 | 59 | * **package.json:** add new deps, remove old, update deps, add more scripts ([96cc268](https://github.com/Metnew/tiny-universal-skeleton/commit/96cc268)) 60 | * **package.json:** install chalk, remove jest config ([a249b09](https://github.com/Metnew/tiny-universal-skeleton/commit/a249b09)) 61 | * **server:** add console.log with chalk ([a78f809](https://github.com/Metnew/tiny-universal-skeleton/commit/a78f809)) 62 | * **tests:** add basic tests ([f8d1a3a](https://github.com/Metnew/tiny-universal-skeleton/commit/f8d1a3a)) 63 | * **webpack_config:** split client's webpack config in dev,prod,base ([51704f7](https://github.com/Metnew/tiny-universal-skeleton/commit/51704f7)) 64 | * **webpack_config:** split server's webpack config in dev,prod,base ([cc28446](https://github.com/Metnew/tiny-universal-skeleton/commit/cc28446)) 65 | 66 | 67 | 68 | 69 | # 0.0.0 (2017-09-12) 70 | 71 | 72 | ### Bug Fixes 73 | 74 | * **webpack_config:** remove eslint errors in server config ([6ac3049](https://github.com/Metnew/tiny-universal-skeleton/commit/6ac3049)) 75 | 76 | 77 | ### Features 78 | 79 | * **.babelrc:** add basic babelrc ([9ae3b06](https://github.com/Metnew/tiny-universal-skeleton/commit/9ae3b06)) 80 | * **.eslintrc.json:** add basic eslintrc ([e72034b](https://github.com/Metnew/tiny-universal-skeleton/commit/e72034b)) 81 | * **common:** add component to render ([ed4f594](https://github.com/Metnew/tiny-universal-skeleton/commit/ed4f594)) 82 | * **src/client:** add basic client entry ([f995b40](https://github.com/Metnew/tiny-universal-skeleton/commit/f995b40)) 83 | * add package.json ([9e6b3d4](https://github.com/Metnew/tiny-universal-skeleton/commit/9e6b3d4)) 84 | * **server:** add API ([f15114a](https://github.com/Metnew/tiny-universal-skeleton/commit/f15114a)) 85 | * **server:** add basic html template ([8927bc9](https://github.com/Metnew/tiny-universal-skeleton/commit/8927bc9)) 86 | * **server:** add basic ssr ([41112dd](https://github.com/Metnew/tiny-universal-skeleton/commit/41112dd)) 87 | * **server:** add middlewares ([a87d44c](https://github.com/Metnew/tiny-universal-skeleton/commit/a87d44c)) 88 | * **server:** add server decorator ([604dc95](https://github.com/Metnew/tiny-universal-skeleton/commit/604dc95)) 89 | * **server:** add server entry ([b3a6326](https://github.com/Metnew/tiny-universal-skeleton/commit/b3a6326)) 90 | * **webpack_config:** add basic webpack config for client ([1cafafd](https://github.com/Metnew/tiny-universal-skeleton/commit/1cafafd)) 91 | * **webpack_config:** add basic webpack config for server ([b3761fe](https://github.com/Metnew/tiny-universal-skeleton/commit/b3761fe)) 92 | * **webpack_config:** add config file ([3cc35c7](https://github.com/Metnew/tiny-universal-skeleton/commit/3cc35c7)) 93 | * **webpack_config:** add dev server ([3392ed4](https://github.com/Metnew/tiny-universal-skeleton/commit/3392ed4)) 94 | * **webpack_config:** add shared webpack config ([6cf0f94](https://github.com/Metnew/tiny-universal-skeleton/commit/6cf0f94)) 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tiny-universal-skeleton 2 | 3 | > ### If you want advanced starter with this feature -> take a look at [SUIcrux](https://github.com/Metnew/suicrux)! 4 | 5 | ![](./example.gif) 6 | 7 | Imagine that you have server, development server and client app: 8 | 9 | - **Server** - your server _middlewares_, _api endpoints_ and _SSR_. 10 | - **Development server** - express server with `webpack-hot-middleware` and `webpack-dev-middleware`. 11 | - **Client** - your frontend. (e.g. React app). 12 | 13 | **Main problem**: sync **server middlewares** with **client** and don't lose **power of webpack-(dev|hot)-middleware.** 14 | 15 | ##### Typical use case 16 | **Your server has middleware that checks is a user logged in and use this info later for SSR.** 17 | 18 | > There are other solutions like [universal-webpack](https://github.com/catamphetamine/universal-webpack).
19 | > I'm not sure what's going on inside `universal-webpack` and can it solve the described above problem. But it **looks complicated**. 20 | 21 | > In case of a bug inside a complicated software with low community support you'll be the one person who cares about this bug. 22 | 23 | This solution is **very-very simple**. But it's not the best, of course. 24 | > 99.9% of universal starters/frameworks run 2 process on 2 ports with 2 different FS. We run only one process on one port with the same FS. 25 | 26 | TL;DR: 27 | 28 | 1. Server entry **should export function that decorates server with all middlewares**, api endpoints, ssr. _Except dev middlewares._ 29 | 2. Compile **both server and client with Webpack**. 30 | 3. Get **compiled decorator** for server and **decorate your dev server**. _DevServer = express server with development middlewares._ 31 | 4. ???????????? 32 | 5. Every time your code changes, webpack recompiles your code and decorates your server with newly compiled code. 33 | 34 | ## Server Entry (src/server/index.js) 35 | 36 | ```javascript 37 | import express from 'express' 38 | import chalk from 'chalk' 39 | 40 | const app = express() 41 | const PORT = 3000 42 | const isProduction = process.env.NODE_ENV === 'production' 43 | const pathToServerDecorator = isProduction 44 | ? './decorator' 45 | : '../../webpack_config/devServer' 46 | // NOTE: Such dynamic imports is a bad practice! 47 | // It's used here to show that our `serverDecorator` is a dynamic thing. 48 | const serverDecorator = require(pathToServerDecorator).default 49 | serverDecorator(app) 50 | 51 | app.listen(PORT, () => { 52 | console.log(chalk.green(`SERVER IS LISTENING ON ${PORT}`)) 53 | }) 54 | ``` 55 | 56 | Did you notice `serverDecorator()` function? Let's figure out what's the hack is it: 57 | 58 | ## Main server decorator (src/server/decorator.js) 59 | 60 | This function decorates express server instance with your middlewares, API and SSR stuff. (e.g. makes your server _yours_) We require this function to decorate our server in production. 61 | 62 | ```javascript 63 | import addMiddlewares from './middlewares' 64 | import API from './api' 65 | import SSR from './ssr' 66 | 67 | /** 68 | * Mount API, SSR and middlewares to app. 69 | * @param {Object} app - Express server instance 70 | * @return {Object} - Decorated server instance 71 | */ 72 | export default function (app) { 73 | // Add global middlewares 74 | addMiddlewares(app) 75 | // Add API 76 | app.use('/api/v1', API) 77 | // Add SSR 78 | app.use(SSR) 79 | return app 80 | } 81 | ``` 82 | 83 | ## Server decorator in development (webpack_config/devServer.js) 84 | 85 | TL;DR: after every compilation we remove already applied middlewares from Express app (from middlewares stack) and apply new ones. 86 | 87 | In development our server decorator looks like: 88 | 89 | ```javascript 90 | // NOTE: Every time we apply our compiled code to development server 91 | // We add new middlewares from new code, but don't remove old middlewares from old code 92 | // Number of middlewares that our app should have 93 | let prevSize = null 94 | /** 95 | * @desc Adds dev middlewares + your code to an express server instance 96 | * @param {ExpressServer} app - Express dev server to which compiled code will be applied 97 | */ 98 | export default function (app) { 99 | /** 100 | * @desc Function that executes after your server-side code compiles 101 | * @param {Function} serverSideCode - compiled server-side code 102 | */ 103 | const done = serverSideCode => { 104 | // Get current stack of the app (e.g. applied to Express server middlewares) 105 | const {stack} = app._router 106 | // get current lenght of stack 107 | const {length} = stack 108 | // When we run server first time we don't have any applied middlewares from compiled code 109 | prevSize = prevSize || length 110 | if (length > prevSize) { 111 | // TL;DR: Remove already compiled code 112 | // That means that we already applied our code to devServer 113 | // And we can remove already applied middlewares from the last compilation 114 | app._router.stack = stack.slice(0, prevSize) 115 | } 116 | // Apply newly compiled code to our app 117 | serverSideCode(app) 118 | } 119 | 120 | // webpack Compiler for server 121 | const serverCompiler = compiler.compilers.find( 122 | compiler => compiler.name === 'server' 123 | ) 124 | // webpack Compiler for client 125 | const clientCompiler = compiler.compilers.find( 126 | compiler => compiler.name === 'client' 127 | ) 128 | // Add webpack-dev-middleware 129 | app.use(devMiddleWare) 130 | // Add webpack-hot-middleware 131 | app.use( 132 | webpackHotMiddleware(clientCompiler, { 133 | log: console.log 134 | }) 135 | ) 136 | // Run `done` function after serverCompiler emits `done` event with a newly compiled code as argument 137 | webpackGetCodeOnDone(serverCompiler, done) 138 | } 139 | ``` 140 | 141 | ## Bundle server with Webpack 142 | 143 | I don't want to argue about **"Is it ok to bundle server-side code with Webpack?"** Shortly, **it's a great idea.** 144 | **Main features**: 145 | - tree-shaking, 146 | - code optimizations, 147 | - high configuration possibilities with `webpack.DefinePlugin()`. 148 | 149 | **Main cons**: it's hard to work with dirs, because webpack supports `__dirname` not as you expected. But it's easy to solve this problem with webpack `alias`. Read more in webpack docs. 150 | 151 | ### Webpack.config 152 | 153 | Your webpack.config.js for server may looks like: 154 | 155 | ```javascript 156 | import path from 'path' 157 | import fs from 'fs' 158 | import webpack from 'webpack' 159 | 160 | const isProduction = process.env.NODE_ENV === 'production' 161 | const distPath = 'my/dist/path' 162 | // NOTE: Notice these lines also: 163 | const entry = isProduction 164 | ? path.join(config.srcPath, './server') 165 | : path.join(config.srcPath, './server/server') 166 | 167 | // Read more about Webpack for server, if you don't know what this lines do. 168 | let nodeModules = {} 169 | fs 170 | .readdirSync('node_modules') 171 | .filter(x => { 172 | return ['.bin'].indexOf(x) === -1 173 | }) 174 | .forEach(mod => { 175 | nodeModules[mod] = 'commonjs ' + mod 176 | }) 177 | 178 | const baseWebpackConfig = { 179 | name: 'server', 180 | entry, 181 | target: 'node', 182 | output: { 183 | path: path.join(distPath, './server'), 184 | filename: 'index.js', 185 | // NOTE: You should add this line: 186 | libraryTarget: 'commonjs2' 187 | // If you didn't add info about "libraryTarget", setup will not work. 188 | // You should add `libraryTarget` property in dev mode 189 | // it's not a must to have `libraryTarget` in production 190 | }, 191 | externals: nodeModules, 192 | node: { 193 | __dirname: true, 194 | global: true 195 | } 196 | } 197 | 198 | export default baseWebpackConfig 199 | ``` 200 | 201 | There are 2 strange things inside our webpack config: 202 | 203 | ##### `"libraryTarget"` 204 | `"libraryTarget"` must be `"commonjs2"`. (Please, be sure that you didn't forget to set libraryTarget in development!) 205 | ##### Dynamic `"entry"` 206 | As you already know: 207 | **in dev** we use server decorator and decorate our **development(!) server**. 208 | **for production build** we use server decorator to **decorate our server**. 209 | 210 | ### Summary 211 | 212 | You can easily make your universal app hot-reloaded in few lines of code. 213 | Performance is good, there are no benchmarks and comparisons, but it works well even on huge projects. 214 | 215 | ### License 216 | 217 | Apache 2.0 License 218 | 219 | ### Author 220 | 221 | Vladimir Metnew [vladimirmetnew@gmail.com](mailto:vladimirmetnew@gmail.com) 222 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017 Vladimir Metnew 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------