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