├── .gitignore ├── src ├── css │ ├── App.css │ └── Example.css ├── components │ ├── Example.js │ ├── Loading.js │ └── App.js └── index.js ├── .travis.yml ├── .babelrc ├── server ├── index.babel.js ├── index.webpack.js └── render.js ├── LICENSE ├── webpack ├── server.prod.js ├── server.dev.js ├── client.dev.js └── client.prod.js ├── .eslintrc.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | buildClient 4 | buildServer -------------------------------------------------------------------------------- /src/css/App.css: -------------------------------------------------------------------------------- 1 | .title { 2 | color: blue; 3 | font-size: 42px; 4 | } -------------------------------------------------------------------------------- /src/css/Example.css: -------------------------------------------------------------------------------- 1 | .paragraph { 2 | color: purple; 3 | font-size: 32px; 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | notifications: 5 | email: false 6 | cache: yarn 7 | script: 8 | - node_modules/.bin/travis-github-status lint -------------------------------------------------------------------------------- /src/components/Example.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styles from '../css/Example.css' 3 | 4 | export default () => ( 5 |
LOADED!
6 | ) 7 | -------------------------------------------------------------------------------- /src/components/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default ({ isLoading, pastDelay, error }) => { 4 | if (isLoading && pastDelay) { 5 | return

Loading...

6 | } 7 | else if (error && !isLoading) { 8 | return

Error!

9 | } 10 | 11 | return null 12 | } 13 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-2"], 3 | "plugins": ["dynamic-import-webpack", [ 4 | "css-modules-transform", { 5 | "generateScopedName": "[name]__[local]--[hash:base64:5]" 6 | } 7 | ], [ 8 | "react-loadable/babel", { 9 | "server": true, 10 | "webpack": true 11 | } 12 | ] 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import AppContainer from 'react-hot-loader/lib/AppContainer' 4 | import App from './components/App' 5 | 6 | const render = App => 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ) 13 | 14 | if (process.env.NODE_ENV === 'development') { 15 | module.hot.accept('./components/App.js', () => { 16 | const App = require('./components/App').default 17 | render(App) 18 | }) 19 | } 20 | 21 | render(App) 22 | -------------------------------------------------------------------------------- /server/index.babel.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import webpack from 'webpack' 3 | import webpackDevMiddleware from 'webpack-dev-middleware-multi-compiler' 4 | import webpackHotMiddleware from 'webpack-hot-middleware' 5 | import clientConfig from '../webpack/client.dev' 6 | import serverRender from './render' 7 | 8 | const DEV = process.env.NODE_ENV === 'development' 9 | const publicPath = clientConfig.output.publicPath 10 | const outputPath = clientConfig.output.path 11 | const app = express() 12 | 13 | if (DEV) { 14 | const compiler = webpack(clientConfig) 15 | 16 | app.use(webpackDevMiddleware(compiler, { publicPath })) 17 | app.use(webpackHotMiddleware(compiler)) 18 | compiler.plugin('done', stats => { 19 | app.use(serverRender({ clientStats: stats.toJson(), outputPath })) 20 | }) 21 | } 22 | else { 23 | const clientStats = require('../buildClient/stats.json') 24 | 25 | app.use(publicPath, express.static(outputPath)) 26 | app.use(serverRender({ clientStats, outputPath })) 27 | } 28 | 29 | app.listen(3000, () => { 30 | console.log('Listening @ http://localhost:3000/') 31 | }) 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 James Gillmore 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /server/index.webpack.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import webpack from 'webpack' 3 | import webpackDevMiddleware from 'webpack-dev-middleware-multi-compiler' 4 | import webpackHotMiddleware from 'webpack-hot-middleware' 5 | import webpackHotServerMiddleware from 'webpack-hot-server-middleware' 6 | import clientConfig from '../webpack/client.dev' 7 | import serverConfig from '../webpack/server.dev' 8 | 9 | const DEV = process.env.NODE_ENV === 'development' 10 | const publicPath = clientConfig.output.publicPath 11 | const outputPath = clientConfig.output.path 12 | const app = express() 13 | 14 | if (DEV) { 15 | const multiCompiler = webpack([clientConfig, serverConfig]) 16 | const clientCompiler = multiCompiler.compilers[0] 17 | 18 | app.use(webpackDevMiddleware(multiCompiler, { publicPath })) 19 | app.use(webpackHotMiddleware(clientCompiler)) 20 | app.use( 21 | webpackHotServerMiddleware(multiCompiler, { 22 | serverRendererOptions: { outputPath } 23 | }) 24 | ) 25 | } 26 | else { 27 | const clientStats = require('../buildClient/stats.json') 28 | const serverRender = require('../buildServer/main.js').default 29 | 30 | app.use(publicPath, express.static(outputPath)) 31 | app.use(serverRender({ clientStats, outputPath })) 32 | } 33 | 34 | app.listen(3000, () => { 35 | console.log('Listening @ http://localhost:3000/') 36 | }) 37 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import React from 'react' 3 | import ReactLoadable from 'react-loadable' 4 | import Loading from './Loading' 5 | import styles from '../css/App.css' 6 | 7 | export default class App extends React.Component { 8 | // set `show` to `true` to see dynamic chunks served by initial request 9 | // set `show` to `false` to test how asynchronously loaded chhunks behave, 10 | // specifically how css injection is embedded in chunks + corresponding HMR 11 | state = { 12 | show: true 13 | } 14 | 15 | componentDidMount() { 16 | setTimeout(() => { 17 | console.log('showing ') 18 | this.setState({ show: true }) 19 | }, 2000) 20 | } 21 | 22 | render() { 23 | return ( 24 |
25 |

Hello World

26 | {this.state.show && } 27 |
28 | ) 29 | } 30 | } 31 | 32 | const Example = ReactLoadable({ 33 | loader: () => fakeDelay(1200).then(() => import('./Example')), 34 | LoadingComponent: Loading 35 | 36 | // if we weren't using the React Loadable babel plugin, you'd need this: 37 | // serverSideRequirePath: path.resolve(__dirname, './Example'), 38 | // webpackRequireWeakId: () => require.resolveWeak('./Example') 39 | }) 40 | 41 | const fakeDelay = ms => new Promise(resolve => setTimeout(resolve, ms)) 42 | -------------------------------------------------------------------------------- /webpack/server.prod.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | 5 | const res = p => path.resolve(__dirname, p) 6 | 7 | const modeModules = res('../node_modules') 8 | const entry = res('../server/render.js') 9 | const output = res('../buildServer') 10 | 11 | module.exports = { 12 | name: 'server', 13 | target: 'node', 14 | devtool: 'source-map', 15 | entry: [entry], 16 | output: { 17 | path: output, 18 | filename: '[name].js', 19 | libraryTarget: 'commonjs2' 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.js$/, 25 | exclude: /node_modules/, 26 | use: 'babel-loader' 27 | } 28 | // if you are rendering the server with webpack, you can use 29 | // css-loader/locals instead of "css-modules-transform"" in .babelrc: 30 | // { 31 | // test: /\.css$/, 32 | // exclude: /node_modules/, 33 | // use: 'css-loader/locals?modules&localIdentName=[name]__[local]--[hash:base64:5]', 34 | // }, 35 | ] 36 | }, 37 | plugins: [ 38 | new webpack.HashedModuleIdsPlugin(), 39 | new webpack.optimize.LimitChunkCountPlugin({ 40 | maxChunks: 1 41 | }), 42 | 43 | new webpack.DefinePlugin({ 44 | 'process.env': { 45 | NODE_ENV: JSON.stringify('production') 46 | } 47 | }) 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /webpack/server.dev.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | 5 | const res = p => path.resolve(__dirname, p) 6 | 7 | const modeModules = res('../node_modules') 8 | const entry = res('../server/render.js') 9 | const output = res('../buildServer') 10 | 11 | // if you're specifying externals to leave unbundled, you need to 12 | // tell Webpack to still bundle `react-loadable` and 13 | // `webpack-flush-chunks` so that they know they are running 14 | // within Webpack and can properly make connections to client modules: 15 | const externals = fs 16 | .readdirSync(modeModules) 17 | .filter(x => !/\.bin|react-loadable|webpack-flush-chunks/.test(x)) 18 | .reduce((externals, mod) => { 19 | externals[mod] = `commonjs ${mod}` 20 | return externals 21 | }, {}) 22 | 23 | module.exports = { 24 | name: 'server', 25 | target: 'node', 26 | devtool: 'source-map', 27 | entry: [entry], 28 | externals, 29 | output: { 30 | path: output, 31 | filename: '[name].js', 32 | libraryTarget: 'commonjs2', 33 | publicPath: '/static/' 34 | }, 35 | module: { 36 | rules: [ 37 | { 38 | test: /\.js$/, 39 | exclude: /node_modules/, 40 | use: 'babel-loader' 41 | } 42 | // if you are rendering the server with webpack, you can use 43 | // css-loader/locals instead of "css-modules-transform"" in .babelrc: 44 | // { 45 | // test: /\.css$/, 46 | // exclude: /node_modules/, 47 | // use: 'css-loader/locals?modules&localIdentName=[name]__[local]--[hash:base64:5]', 48 | // }, 49 | ] 50 | }, 51 | plugins: [ 52 | new webpack.NamedModulesPlugin(), 53 | new webpack.optimize.LimitChunkCountPlugin({ 54 | maxChunks: 1 55 | }), 56 | 57 | new webpack.DefinePlugin({ 58 | 'process.env': { 59 | NODE_ENV: JSON.stringify('development') 60 | } 61 | }) 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'babel-eslint', 3 | parserOptions: { 4 | ecmaFeatures: { 5 | generators: true, 6 | experimentalObjectRestSpread: true 7 | }, 8 | sourceType: 'module', 9 | allowImportExportEverywhere: false 10 | }, 11 | plugins: ['flowtype'], 12 | extends: ['airbnb', 'plugin:flowtype/recommended'], 13 | settings: { 14 | flowtype: { 15 | onlyFilesWithFlowAnnotation: true 16 | } 17 | }, 18 | globals: { 19 | window: true, 20 | document: true, 21 | __dirname: true, 22 | __DEV__: true, 23 | CONFIG: true, 24 | process: true, 25 | jest: true, 26 | describe: true, 27 | test: true, 28 | it: true, 29 | expect: true, 30 | beforeEach: true 31 | }, 32 | 'import/resolver': { 33 | node: { 34 | extensions: ['.js', '.css', '.json', '.styl'] 35 | } 36 | }, 37 | 'import/extensions': ['.js'], 38 | 'import/ignore': ['node_modules', 'flow-typed', '\\.(css|styl|svg|json)$'], 39 | rules: { 40 | 'no-shadow': 0, 41 | 'no-use-before-define': 0, 42 | 'no-param-reassign': 0, 43 | 'react/prop-types': 0, 44 | 'react/no-render-return-value': 0, 45 | 'no-confusing-arrow': 0, 46 | camelcase: 1, 47 | 'prefer-template': 1, 48 | 'react/no-array-index-key': 1, 49 | 'global-require': 1, 50 | 'react/jsx-indent': 1, 51 | 'dot-notation': 1, 52 | 'import/no-named-default': 1, 53 | 'no-unused-vars': 1, 54 | 'import/no-unresolved': 1, 55 | semi: [2, 'never'], 56 | 'flowtype/semi': [2, 'never'], 57 | 'jsx-quotes': [2, 'prefer-single'], 58 | 'react/jsx-filename-extension': [2, { extensions: ['.jsx', '.js'] }], 59 | 'spaced-comment': [2, 'always', { markers: ['?'] }], 60 | 'arrow-parens': [2, 'as-needed', { requireForBlockBody: false }], 61 | 'brace-style': [2, 'stroustrup'], 62 | 'import/no-extraneous-dependencies': [ 63 | 'error', 64 | { 65 | devDependencies: true, 66 | optionalDependencies: true, 67 | peerDependencies: true 68 | } 69 | ], 70 | 'comma-dangle': [ 71 | 2, 72 | { 73 | arrays: 'never', 74 | objects: 'never', 75 | imports: 'never', 76 | exports: 'never', 77 | functions: 'never' 78 | } 79 | ] 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /server/render.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import React from 'react' 3 | import ReactDOM from 'react-dom/server' 4 | import * as ReactLoadable from 'react-loadable' 5 | import flushChunks from 'webpack-flush-chunks' 6 | import App from '../src/components/App' 7 | 8 | export default ({ clientStats, outputPath }) => (req, res, next) => { 9 | const app = ReactDOM.renderToString() 10 | const moduleIds = flushRequires() 11 | 12 | const { 13 | // react components: 14 | Js, 15 | Styles, // external stylesheets 16 | Css, // raw css 17 | 18 | // strings: 19 | js, 20 | styles, // external stylesheets 21 | css, // raw css 22 | 23 | // arrays of file names (not including publicPath): 24 | scripts, 25 | stylesheets 26 | } = flushChunks(moduleIds, clientStats, { 27 | before: ['bootstrap'], 28 | after: ['main'], 29 | 30 | // only needed if using babel on the server 31 | rootDir: path.resolve(__dirname, '..'), 32 | 33 | // only needed if serving css rather than an external stylesheet 34 | outputPath 35 | }) 36 | 37 | console.log('SERVED SCRIPTS', scripts) 38 | console.log('SERVED STYLESHEETS', stylesheets) 39 | 40 | res.send( 41 | ` 42 | 43 | 44 | 45 | react-loadable-example 46 | 47 | 48 | ${styles} 49 |
${app}
50 | ${js} 51 | 52 | ` 53 | ) 54 | 55 | // COMMENT the above `res.send` call 56 | // and UNCOMMENT below to test rendering React components: 57 | 58 | // const html = ReactDOM.renderToStaticMarkup( 59 | // 60 | // 61 | // 62 | // 63 | // 64 | //
65 | // 66 | // 67 | // 68 | // ) 69 | 70 | // res.send(`${html}`) 71 | } 72 | 73 | const IS_WEBPACK = typeof __webpack_require__ !== 'undefined' 74 | 75 | // to be replaced /w `ReactLoadable.flushRequires` if PR is accepted 76 | const flushRequires = () => 77 | (IS_WEBPACK 78 | ? ReactLoadable.flushWebpackRequireWeakIds() 79 | : ReactLoadable.flushServerSideRequirePaths()) 80 | -------------------------------------------------------------------------------- /webpack/client.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const ExtractCssChunks = require('extract-css-chunks-webpack-plugin') 4 | 5 | module.exports = { 6 | name: 'client', 7 | target: 'web', 8 | devtool: 'source-map', 9 | entry: [ 10 | 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000&reload=false&quiet=false&noInfo=false', 11 | 'react-hot-loader/patch', 12 | path.resolve(__dirname, '../src/index.js') 13 | ], 14 | output: { 15 | filename: '[name].js', 16 | path: path.resolve(__dirname, '../buildClient'), 17 | publicPath: '/static/' 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | // Note: this just as easily could have been a more regular looking usage 23 | // of the babel-loader, but because this example package showcases both 24 | // a babel server and a webpack server (and because it's likely better to use 25 | // a babel plugin rather than extract-text-webpack-plugin to transform CSS 26 | // requires server-side anyway), we must override the .babelrc file here, 27 | // doing exactly what it normally does without the `css-modules-transform` 28 | // plugin since the webpack css-loader handles it below. 29 | test: /\.js$/, 30 | exclude: /node_modules/, 31 | use: { 32 | loader: 'babel-loader', 33 | options: { 34 | babelrc: false, 35 | presets: ['es2015', 'react', 'stage-2'], 36 | plugins: [ 37 | 'babel-plugin-dynamic-import-webpack', 38 | 'react-hot-loader/babel', 39 | [ 40 | 'react-loadable/babel', 41 | { 42 | server: true, 43 | webpack: true 44 | } 45 | ] 46 | ] 47 | } 48 | } 49 | }, 50 | // remove above and uncomment this if you are not using 51 | // "css-modules-transform" in .babelrc: 52 | // { 53 | // test: /\.js$/, 54 | // exclude: /node_modules/, 55 | // use: 'babel-loader', 56 | // }, 57 | { 58 | test: /\.css$/, 59 | use: ExtractCssChunks.extract({ 60 | use: 'css-loader?modules&localIdentName=[name]__[local]--[hash:base64:5]' 61 | }) 62 | } 63 | ] 64 | }, 65 | plugins: [ 66 | new ExtractCssChunks(), 67 | new webpack.NamedModulesPlugin(), // only needed when server built with webpack 68 | new webpack.optimize.CommonsChunkPlugin({ 69 | names: ['bootstrap'], // needed to put webpack bootstrap code before chunks 70 | filename: '[name].js', 71 | minChunks: Infinity 72 | }), 73 | 74 | new webpack.HotModuleReplacementPlugin(), 75 | new webpack.NoEmitOnErrorsPlugin(), 76 | new webpack.DefinePlugin({ 77 | 'process.env': { 78 | NODE_ENV: JSON.stringify('development') 79 | } 80 | }) 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /webpack/client.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const ExtractCssChunks = require('extract-css-chunks-webpack-plugin') 4 | const StatsPlugin = require('stats-webpack-plugin') 5 | 6 | module.exports = { 7 | name: 'client', 8 | target: 'web', 9 | devtool: 'source-map', 10 | entry: [path.resolve(__dirname, '../src/index.js')], 11 | output: { 12 | filename: '[name].[chunkhash].js', 13 | path: path.resolve(__dirname, '../buildClient'), 14 | publicPath: '/static/' 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | // Note: this just as easily could have been a more regular looking usage 20 | // of the babel-loader, but because this example package showcases both 21 | // a babel server and a webpack server (and because it's likely better to use 22 | // a babel plugin rather than extract-text-webpack-plugin to transform CSS 23 | // requires server-side anyway), we must override the .babelrc file here, 24 | // doing exactly what it normally does without the `css-modules-transform` 25 | // plugin since the webpack css-loader handles it below. 26 | test: /\.js$/, 27 | exclude: /node_modules/, 28 | use: { 29 | loader: 'babel-loader', 30 | options: { 31 | babelrc: false, 32 | presets: ['es2015', 'react', 'stage-2'], 33 | plugins: [ 34 | 'babel-plugin-dynamic-import-webpack', 35 | [ 36 | 'react-loadable/babel', 37 | { 38 | server: true, 39 | webpack: true 40 | } 41 | ] 42 | ] 43 | } 44 | } 45 | }, 46 | // remove above babel-loader and uncomment this if you are rendering the 47 | // server with Babel and not using "css-modules-transform" in .babelrc: 48 | // { 49 | // test: /\.js$/, 50 | // exclude: /node_modules/, 51 | // use: 'babel-loader', 52 | // }, 53 | { 54 | test: /\.css$/, 55 | use: ExtractCssChunks.extract({ 56 | use: 'css-loader?modules&localIdentName=[name]__[local]--[hash:base64:5]' 57 | }) 58 | } 59 | ] 60 | }, 61 | plugins: [ 62 | new ExtractCssChunks(), 63 | new webpack.HashedModuleIdsPlugin(), // only needed when server built with webpack 64 | new webpack.optimize.CommonsChunkPlugin({ 65 | names: ['bootstrap'], // needed to put webpack bootstrap code before chunks 66 | filename: '[name].[chunkhash].js', 67 | minChunks: Infinity 68 | }), 69 | new StatsPlugin('stats.json'), 70 | 71 | new webpack.DefinePlugin({ 72 | 'process.env': { 73 | NODE_ENV: JSON.stringify('production') 74 | } 75 | }), 76 | new webpack.optimize.UglifyJsPlugin({ 77 | compress: { 78 | screw_ie8: true, 79 | warnings: false 80 | }, 81 | mangle: { 82 | screw_ie8: true 83 | }, 84 | output: { 85 | screw_ie8: true, 86 | comments: false 87 | }, 88 | sourceMap: true 89 | }) 90 | ] 91 | } 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flush-requires-boilerplate", 3 | "description": "A boilerplate showing how to achieve Universal Code-Splitting and Universal HMR with react-loadable, webpack-flush-chunks and extract-css-chunks-webpack-plugin", 4 | "version": "1.0.0", 5 | "main": "server/index.js", 6 | "author": "James Gillmore ", 7 | "license": "MIT", 8 | "scripts": { 9 | "start": "npm run start:webpack:dev", 10 | "start:babel:dev": "npm run clean && NODE_ENV=development babel-watch -x src server/index.babel.js", 11 | "start:babel:prod": "npm run build:client && NODE_ENV=production babel-watch -x src server/index.babel.js", 12 | "start:webpack:dev": "npm run clean && NODE_ENV=development babel-watch server/index.webpack.js", 13 | "start:webpack:prod": "npm run build:both && NODE_ENV=production babel-watch server/index.webpack.js", 14 | "build:client": "rimraf buildClient && webpack --progress -p --config webpack/client.prod.js", 15 | "build:server": "rimraf buildServer && webpack --progress -p --config webpack/server.prod.js", 16 | "build:both": "npm run build:client && npm run build:server", 17 | "clean": "rimraf buildClient buildServer", 18 | "precommit": "lint-staged --verbose", 19 | "commit": "git-cz", 20 | "lint": "eslint --fix src server webpack", 21 | "format": "prettier --single-quote --semi=false --write '{src,server,webpack}/**/*.js'" 22 | }, 23 | "dependencies": { 24 | "express": "^4.15.2", 25 | "react": "^15.4.2", 26 | "react-dom": "^15.4.2", 27 | "react-loadable": "^3.3.1", 28 | "webpack-flush-chunks": "^0.0.3" 29 | }, 30 | "devDependencies": { 31 | "babel-cli": "^6.24.0", 32 | "babel-core": "^6.24.0", 33 | "babel-eslint": "^7.2.3", 34 | "babel-loader": "^6.4.0", 35 | "babel-plugin-css-modules-transform": "^1.2.7", 36 | "babel-plugin-dynamic-import-webpack": "^1.0.1", 37 | "babel-preset-es2015": "^6.24.0", 38 | "babel-preset-react": "^6.23.0", 39 | "babel-preset-stage-2": "^6.22.0", 40 | "babel-watch": "^2.0.6", 41 | "commitizen": "^2.9.6", 42 | "css-loader": "^0.27.3", 43 | "cz-conventional-changelog": "^2.0.0", 44 | "eslint": "^3.19.0", 45 | "eslint-config-airbnb": "^14.1.0", 46 | "eslint-plugin-flowtype": "^2.32.1", 47 | "eslint-plugin-import": "^2.2.0", 48 | "eslint-plugin-jsx-a11y": "^4.0.0", 49 | "eslint-plugin-react": "^6.10.3", 50 | "extract-css-chunks-webpack-plugin": "^1.0.8", 51 | "husky": "^0.13.2", 52 | "lint-staged": "^3.4.0", 53 | "prettier": "^1.2.2", 54 | "react-hot-loader": "next", 55 | "rimraf": "^2.6.1", 56 | "stats-webpack-plugin": "^0.5.0", 57 | "travis-github-status": "^1.4.0", 58 | "webpack": "^2.2.1", 59 | "webpack-dev-middleware-multi-compiler": "^1.0.0", 60 | "webpack-hot-middleware": "^2.17.1", 61 | "webpack-hot-server-middleware": "^0.1.0" 62 | }, 63 | "config": { 64 | "commitizen": { 65 | "path": "./node_modules/cz-conventional-changelog" 66 | } 67 | }, 68 | "lint-staged": { 69 | "*.js": [ 70 | "prettier --single-quote --semi=false --write", 71 | "eslint --fix", 72 | "git add" 73 | ] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flush Chunks Boilerplate 2 | 3 | This is a boilerplate example for how to use [webpack-flush-chunks](https://github.com/faceyspacey/webpack-flush-chunks) 4 | in conjunction with [react-loadable](https://github.com/thejameskyle/react-loadable) and [extract-css-chunks-webpack-plugin](https://github.com/faceyspacey/extract-css-chunks-webpack-plugin). 5 | 6 | You should get familiar with the documentation of those while running this. 7 | 8 | ***Note: This is a preemptive repo. For HMR to work for split components a pending [PR](https://github.com/thejameskyle/react-loadable/pull/37) to *React Loadable* will have to be accepted. That PR also contains a suggestion for the `flushRequires` API documented below. Currently in code the original flushing API is used. Don't sweat it.*** 9 | 10 | 11 | ## Installation 12 | 13 | ``` 14 | git clone https://github.com/faceyspacey/flush-chunks-boilerplate.git 15 | cd flush-chunks-boilerplate 16 | yarn install 17 | ``` 18 | 19 | ## Usage 20 | 21 | *try out universal Webpack bundling:* 22 | ``` 23 | yarn run start:webpack:dev 24 | yarn run start:webpack:prod 25 | ``` 26 | 27 | *use Babel ont the server:* 28 | ``` 29 | yarn run start:babel:dev 30 | yarn run start:babel:pro 31 | ``` 32 | 33 | After selecting one of the above commands, open [localhost:3000](http://localhost:3000) in your browser. We recommend you start with Webpack in development as that offers the best developer experience. After check it in the production and see what files exist within your bundle. And often view the source in the browser to see what chunks are being sent. 34 | 35 | 36 | ## Things To Do 37 | 38 | - try all 4 commands and examine their corresponding Webpack configs and their corresponding server files: [`server/index.webpack.js`](./server/index.webpack.js) and [`server/index.babel.js`](./server/index.babel.js) 39 | - view the source in your browser to see what the server sends (do this often) 40 | - open [`src/components/App.js`](./src/components/App.js) and toggle `state.show` between `false` and `true` and 41 | then view the source in your browser to see when corresponding chunks are sent vs. not sent. 42 | - open the browser console *Network* tab and see when in fact the `Example.js` chunk is asynchronously loaded (it won't be if `state.show` starts off as `true`, which is desired result, i.e. because the client *synchronously* renders exactly what was rendered on the server) 43 | - edit the `` and `` components to see that HMR works--even for split chunks. Do so with both `state.show` pre-set to both 44 | `false` and `true` to verify HMR works in all scenarios. 45 | - edit and save the CSS files to confirm HMR works for CSS as well, thanks to [extract-css-chunks-webpack-plugin](https://github.com/faceyspacey/extract-css-chunks-webpack-plugin) 46 | 47 | - when bundling for production, examine the `buildClient` and `buildServer` folders to see exactly what chunks and files are built for you. Notice the `stats.json` file in `buildClient`. Notice that every javascript chunk has 2 versions of itself: one ending with the extension `.js` and one ending with `.no_css.js`. This is thanks to 48 | [extract-css-chunks-webpack-plugin](https://github.com/faceyspacey/extract-css-chunks-webpack-plugin) which prepares an additional javascript file with no CSS (for the smallest possible build sizes) for sending over the wire as part of SSR. The regular one with css injection in it is used when the chunk is asynchronously loaded as your users navigate your app. HMR will work in both cases. 49 | - open [`server/render.js`](./server/render.js) and follow the directions of what lines to comment and uncomment to test rendering your chunks as strings vs. React components. 50 | - open [`server/render.js`](./server/render.js) and from the return of `flushCHunks` use `css` instead of `styles` while running in production to see your CSS embedded directly in the response page. View the source in your browser to confirm. *Note: during development, external stylesheets will still be used for HMR to work; this is automatic.* 51 | - check out the amazing package, [webpack-hot-server-middleware](https://github.com/60frames/webpack-hot-server-middleware), to see how dual-compilation works for both your server and client bundles. It's very fast because it shares compiled files between the 2 bundles. We've been surprised this hasn't been more popular and the defacto solution for rendering both bundles. It's very solid. We think you will like it...Another thing nice about it is how seamlessly it ushers your compilation stats to the render function you pass to express's `app.use`. 52 | - **remove the code that doesn't suit your particular scenario, and start your app!** 53 | 54 | 55 | ## Server-Rendering (Babel vs. Webpack) 56 | 57 | When you end up using this in your actual app, you will likely end up deleting much of the code that is not relevant for your particular use case. For example, if 58 | you're rendering both the client and server with Webpack, you will delete `index.babel.js` and `"css-modules-transform"` from your `.babelrc`. And if you're rendering the server with Babel, you will delete the `server.dev/prod.js` Webpack configs. In addition, you will likely simplify your use of `flushChunks` to only return the aspects you decide on using, and uncomment a few things like `'css-loader/locals?...'` if you're bundling the server with Webpack. 59 | 60 | If you're not embedding CSS directly in your response strings, you can forget about ushering the `outputPath` to your `serverRender` function. Keep in mind though that if you do, and if you render the server with Webpack this can become a time-sink to figure out for those not familiar with how Webpack mocks the file system. Basically by default the file system won't be what you expect it to be if you call `path.resolve(__dirname, '..')` within a webpack-compiled portion of your code, which is why it's very nice how [webpack-hot-server-middleware](https://github.com/60frames/webpack-hot-server-middleware) allows you to pass options from Babel code where you can get your bundle's output path resolved properly. Universal Webpack is awesome, but has a few hurdles to doing correctly, particularly in development. [webpack-hot-server-middleware](https://github.com/60frames/webpack-hot-server-middleware) solves those hurdles. 61 | 62 | Hopefully insights from this boilerplate simplifies things for you. The key is to recognize the boundary this boilerplate has chosen between what server code is compiled by Webpack and what code is compiled by Babel. The boundary specifically is [`server/index.webpack.js`](./server/index.webpack.js), which is handled by Babel and [`server/render.js`](./server/render.js), which is handled by Webpack--both of which are run on the server. 63 | 64 | If you haven't rendered your server with Webpack before, now's a good time to give it a try. Helping make that--along with *complete HMR*--more of a mainstream thing is a side aim of this repo. 65 | 66 | 67 | ## Final Note: Hot Module Replacement 68 | 69 | You will notice that the server code is watched with `babel-watch` in `package.json`. The goal is obviously HMR everywhere, since no matter what some of your code is built outside of Webpack. 70 | 71 | There is one gotcha with that though: if you edit the server code (not compiled by Webpack), it will update, but then connection to the client will be lost, and you need to do a refresh. This is very useful for cases where you are actively refreshing, such as when you're checking the output from you server in your browser source code tab, but obviously not the pinnacle in universal HMR. 72 | 73 | However, when your not editing your `express` code much, and if you're editing webpack-compiled code (whether rendered on the client or server), HMR will isomorphically work with no unexpected hiccups; and that *awesomeness* is likely what you'll experience most of the time. That's one of the key benefits of [webpack-hot-server-middleware](https://github.com/60frames/webpack-hot-server-middleware). 74 | 75 | If you have a solution to reconnecting the client to HMR after `babel-watch` reloads the server code, we'd love to hear it. 76 | 77 | *Long live the dreams of Universal HMR* and ***Universal Code-Splitting!*** 78 | 79 | 80 | ## Contributing 81 | We use [commitizen](https://github.com/commitizen/cz-cli), so run `npm run commit` to make commits. A command-line form will appear, requiring you answer a few questions to automatically produce a nicely formatted commit. 82 | 83 | --------------------------------------------------------------------------------