├── .babelrc ├── .env_example ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .nvmrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── public ├── favicon.ico └── robots.txt ├── src ├── client │ └── index.js ├── server │ └── index.js ├── shared │ └── universal │ │ ├── components │ │ └── App │ │ │ ├── App.js │ │ │ ├── globals.css │ │ │ ├── index.js │ │ │ ├── lib │ │ │ └── Logo │ │ │ │ ├── Logo.js │ │ │ │ ├── index.js │ │ │ │ └── logo.png │ │ │ └── views │ │ │ ├── About │ │ │ ├── About.js │ │ │ └── index.js │ │ │ └── Home │ │ │ ├── Home.js │ │ │ └── index.js │ │ ├── routes │ │ └── index.js │ │ └── utils │ │ └── guards.js └── universalMiddleware │ ├── clientAssets.js │ ├── index.js │ └── render.js └── tools ├── config └── envVars.js ├── development ├── createNotification.js ├── hotClient.js ├── hotServer.js ├── index.js ├── listenerManager.js └── universalDevMiddleware.js ├── flow ├── definitions │ ├── commonjs.js │ ├── es6modules.js │ └── react.js ├── flow-typed │ ├── README.md │ └── npm │ │ └── express_v4.x.x.js └── stubs │ └── WebpackAsset.js.flow ├── utils └── index.js └── webpack ├── client.config.js ├── configFactory.js ├── server.config.js └── universalMiddleware.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | // We don't use the .babelrc file for our babel configuration, preferring for 2 | // our webpack loaders to have full control over the babel configuration. 3 | // This allows us to build up our babel config in a much more dynamic fashion 4 | // easily targetting a mix mash of server vs browser vs dev vs prod etc. 5 | // @see webpackConfigFactory.js 6 | {} 7 | -------------------------------------------------------------------------------- /.env_example: -------------------------------------------------------------------------------- 1 | # This is an example environment configuration file. You can create your own 2 | # .env implementation or even just set these values directly on the environment 3 | # via your CI server for example. 4 | 5 | # NOTE! 6 | # These keys will be used by the webpack configFactory. Specifically webpack 7 | # parses the code and will replace any "process.env.{VAR_NAME}" instances with 8 | # the matching value from your vars below. 9 | 10 | # The port on which to run our server. 11 | SERVER_PORT=1337 12 | 13 | # The port on which to run our client bundle dev server. 14 | CLIENT_DEVSERVER_PORT=7331 15 | 16 | # Disable SSR? 17 | DISABLE_SSR=false 18 | 19 | # Where should we output our bundles? (relative to project root) 20 | BUNDLE_OUTPUT_PATH=./build 21 | 22 | # What should we name the file that contains details of all the files contained 23 | # within our bundles? 24 | BUNDLE_ASSETS_FILENAME=assets.json 25 | 26 | # What is the public http path at which we will serve our client bundle from? 27 | CLIENT_BUNDLE_HTTP_PATH=/client/ 28 | 29 | # How long should we set the browser cache for the client bundle assets? Don't 30 | # worry, we add hashes to the files, so if they change the new files will be 31 | # served to browsers. 32 | # We are using the "ms" format to set the length. 33 | # @see https://www.npmjs.com/package/ms 34 | CLIENT_BUNDLE_CACHE_MAXAGE=365d 35 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "plugins": [ 5 | "flow-vars" 6 | ], 7 | "env": { 8 | "browser": true, 9 | "es6": true, 10 | "node": true 11 | }, 12 | "ecmaFeatures": { 13 | "defaultParams": true 14 | }, 15 | "rules": { 16 | # We use the 'import' plugin which allows for cases "flow" awareness. 17 | "no-duplicate-imports": 0, 18 | 19 | "flow-vars/define-flow-type": 1, 20 | "flow-vars/use-flow-type": 1, 21 | 22 | "react/jsx-filename-extension": [0], 23 | 24 | "import/no-extraneous-dependencies": [0], 25 | "import/no-unresolved": [2, { ignore: ['^react(-dom(\/server)?)?$'] }], 26 | "import/extensions": [2, { "react": "never" }] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [include] 2 | 3 | [ignore] 4 | 5 | # Including these files causes issues. 6 | .*/node_modules/fbjs/.* 7 | 8 | [libs] 9 | 10 | # Official "flow-typed" repository definitions. 11 | tools/flow/flow-typed/npm 12 | 13 | # Custom definitions. 14 | tools/flow/definitions/ 15 | 16 | # Note: the following definitions come bundled with flow. It can be handy 17 | # to reference them. 18 | # React: https://github.com/facebook/flow/blob/master/lib/react.js 19 | # Javascript: https://github.com/facebook/flow/blob/master/lib/core.js 20 | # Node: https://github.com/facebook/flow/blob/master/lib/node.js 21 | # DOM: https://github.com/facebook/flow/blob/master/lib/dom.js 22 | # BOM: https://github.com/facebook/flow/blob/master/lib/bom.js 23 | # CSSOM: https://github.com/facebook/flow/blob/master/lib/cssom.js 24 | # IndexDB: https://github.com/facebook/flow/blob/master/lib/indexeddb.js 25 | 26 | [options] 27 | 28 | # This is so that we can import static files in our webpack supported components 29 | # and not have flow throw a hissy fit. 30 | module.name_mapper='^\(.*\)\.\(css\|eot\|gif\|ico\|jpg\|jpeg\|less\|otf\|mp3\|mp4\|ogg\|png\|sass\|scss\|sss\|svg\|swf\|ttf\|webp\|woff\|woff2\)$' -> '/tools/flow/stubs/WebpackAsset.js.flow' 31 | 32 | [version] 33 | 0.32.0 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environment Configuration 2 | .env 3 | 4 | # Build output folders 5 | build/ 6 | 7 | # Logs 8 | logs 9 | *.log 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Dependency directory 23 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 24 | node_modules 25 | 26 | # Debug log from npm 27 | npm-debug.log 28 | 29 | # IntelliJ IDE ignore 30 | .idea 31 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v6.6.0 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## [3.1.0] - 2016-08-11 6 | 7 | ### Changed 8 | 9 | Updated to match v3.3.1 API of react-universally. 10 | 11 | ## [3.0.0] - 2016-08-09 12 | 13 | ### Breaking Changes 14 | 15 | Updated to match v3.0.0 API of react-universally. 16 | 17 | ## [2.0.0] - 2016-08-08 18 | 19 | ### Breaking Changes 20 | 21 | The server side render method has been massively simplified as we are now using 22 | react-helmet to control our page header from our components. 23 | 24 | ### Added 25 | 26 | A 'public' static files endpoint, with a favicon implementation. 27 | 28 | ### Changed 29 | 30 | The server render helper now resolves the 'assets.json' via the webpack configuration for the client bundle. 31 | 32 | ### Fixed 33 | 34 | Small issue with the dev server not hot reloading when just the server code changes. 35 | 36 | The projects dependencies structure so that the "dependencies" section contains ALL libraries that are required for the server runtime. The "devDependencies" section contains the libraries required for building the project. 37 | 38 | ## [1.0.0] - 2016-07-18 39 | 40 | ### Added 41 | 42 | Version 1 of the react-universally-skinny boilerplate. 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Sean Matheson 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### DEPRECATED 2 | 3 | I am no longer managing this repo. I am instead looking to integrate a branch on the [main repo](https://github.com/ctrlplusb/react-universally). 4 | 5 | Beware. This repo is out of date. 6 | 7 | ---------- 8 | 9 |

10 |

React, Universally - Skinny

11 |

12 |

A "when size matters" adaptation of the react-universally starter kit.

13 |

14 | 15 | ## TOC 16 | 17 | - [About](https://github.com/ctrlplusb/react-universally#about) 18 | - [References](https://github.com/ctrlplusb/react-universally#references) 19 | 20 | ## About 21 | 22 | This is an alternative version of [`react-universally`](https://github.com/ctrlplusb/react-universally) (a starter kit that contains all the build tooling and configuration you need to kick off your next universal react project, whilst containing a minimal "project" set up allowing you to make your own architecture decisions). Please reference the [`react-universally`](https://github.com/ctrlplusb/react-universally) documentation for the features contained within this starter kit. 23 | 24 | This adaptation of the starter kit attempts to provide you with as small as a client bundle size as possible whilst maintaining the equivalent features/functionality. It does so by making use of the amazing `preact` and `preact-compat` libraries. 25 | 26 | Take a look at the differences in bundle size output... 27 | 28 | ### [`react-universally`](https://github.com/ctrlplusb/react-universally) 29 | 30 | | Chunk Name | Size (GZipped) | 31 | |-------------------------------|------------------| 32 | | index.js | 72.9 KB | 33 | 34 | ### [`react-universally-skinny`](https://github.com/ctrlplusb/react-universally-skinny) 35 | 36 | | Chunk Name | Size (GZipped) | 37 | |-------------------------------|------------------| 38 | | index.js | 37.4 KB | 39 | 40 | BOOM, ___48%___ size savings! 41 | 42 | Of course these don't come without a cost. As we are using `preact` we have had to drop `react-hot-loader` and instead replace it with a native implementation of `webpack` HMR feature. You still have hot reloading, it's not as powerful, but it's probably good enough. 43 | 44 | ## References ## 45 | 46 | - __react-universally__ - https://github.com/ctrlplusb/react-universally 47 | - __react-router v3 changelog__ - https://github.com/reactjs/react-router/blob/next/CHANGES.md 48 | - __preact__ - https://github.com/developit/preact 49 | - __Webpack 2__ - https://gist.github.com/sokra/27b24881210b56bbaff7 50 | - __React Hot Loader v3__ - https://github.com/gaearon/react-hot-boilerplate/pull/61 51 | - __dotenv__ - https://github.com/bkeepers/dotenv 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-universally-skinny", 3 | "version": "4.0.0", 4 | "description": "A \"when size matters\" adaptation of the react-universally starter kit.", 5 | "main": "build/server/main.js", 6 | "engines": { 7 | "node": "~6.6.0" 8 | }, 9 | "scripts": { 10 | "typecheck": "flow", 11 | "lint": "eslint src", 12 | "clean": "rimraf build", 13 | "development": "node ./tools/development", 14 | "build": "npm run clean && webpack --config ./tools/webpack/client.config.js && webpack --config ./tools/webpack/universalMiddleware.config.js && webpack --config ./tools/webpack/server.config.js", 15 | "start": "node build/server" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/ctrlplusb/react-universally.git" 20 | }, 21 | "keywords": [ 22 | "react", 23 | "preact", 24 | "boilerplate", 25 | "starter kit", 26 | "universal", 27 | "javascript", 28 | "express", 29 | "webpack" 30 | ], 31 | "author": "Sean Matheson ", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/ctrlplusb/react-universally/issues" 35 | }, 36 | "homepage": "https://github.com/ctrlplusb/react-universally#readme", 37 | "dependencies": { 38 | "app-root-path": "2.0.1", 39 | "compression": "1.6.2", 40 | "express": "4.14.0", 41 | "helmet": "2.2.0", 42 | "hpp": "0.2.1", 43 | "normalize.css": "4.2.0", 44 | "path": "0.12.7", 45 | "preact": "6.2.1", 46 | "preact-compat": "3.6.0", 47 | "react-helmet": "3.1.0", 48 | "react-router": "2.8.1", 49 | "serialize-javascript": "1.3.0", 50 | "source-map-support": "0.4.3" 51 | }, 52 | "devDependencies": { 53 | "assets-webpack-plugin": "3.4.0", 54 | "babel-cli": "6.16.0", 55 | "babel-core": "6.16.0", 56 | "babel-eslint": "7.0.0", 57 | "babel-loader": "6.2.5", 58 | "babel-preset-latest": "6.16.0", 59 | "babel-preset-react": "6.16.0", 60 | "chokidar": "1.6.0", 61 | "colors": "1.1.2", 62 | "css-loader": "0.25.0", 63 | "dotenv": "2.0.0", 64 | "eslint": "3.6.1", 65 | "eslint-config-airbnb": "12.0.0", 66 | "eslint-plugin-flow-vars": "0.5.0", 67 | "eslint-plugin-import": "2.0.0", 68 | "eslint-plugin-jsx-a11y": "2.2.2", 69 | "eslint-plugin-react": "6.3.0", 70 | "extract-text-webpack-plugin": "2.0.0-beta.4", 71 | "file-loader": "0.9.0", 72 | "flow-bin": "0.32.0", 73 | "json-loader": "0.5.4", 74 | "node-notifier": "4.6.1", 75 | "regenerator-runtime": "0.9.5", 76 | "rimraf": "2.5.4", 77 | "style-loader": "0.13.1", 78 | "url-loader": "0.5.7", 79 | "webpack": "2.1.0-beta.25", 80 | "webpack-dev-middleware": "1.8.3", 81 | "webpack-hot-middleware": "2.12.2", 82 | "webpack-md5-hash": "0.0.5", 83 | "webpack-node-externals": "1.4.3" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/react-universally-skinny/664841f607f748d1c0c84ba2ee503f09dd9b79cb/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import { render } from 'react-dom'; 5 | import Router from 'react-router/lib/Router'; 6 | import browserHistory from 'react-router/lib/browserHistory'; 7 | import match from 'react-router/lib/match'; 8 | import routes from '../shared/universal/routes'; 9 | 10 | // Get the DOM Element that will host our React application. 11 | const container = document.querySelector('#app'); 12 | 13 | function routerError(error) { 14 | console.error('==> Router match failed.'); // eslint-disable-line no-console 15 | if (error) { console.error(error); } // eslint-disable-line no-console 16 | } 17 | 18 | function renderApp(appRoutes) { 19 | // As we are using dynamic react-router routes we have to use the following 20 | // asynchronous routing mechanism supported by the `match` function. 21 | // @see https://github.com/reactjs/react-router/blob/master/docs/guides/ServerRendering.md 22 | match({ history: browserHistory, routes: appRoutes }, (error, redirectLocation, renderProps) => { 23 | if (error) { 24 | routerError(error); 25 | } else if (redirectLocation) { 26 | return; 27 | } else if (renderProps) { 28 | render( 29 | // We need to explicly render the Router component here instead of have 30 | // this embedded within a shared App type of component as we use different 31 | // router base components for client vs server rendering. 32 | , 33 | container 34 | ); 35 | } else { 36 | routerError(); 37 | } 38 | 39 | render( 40 | // We need to explicly render the Router component here instead of have 41 | // this embedded within a shared App type of component as we use different 42 | // router base components for client vs server rendering. 43 | , 44 | container 45 | ); 46 | }); 47 | } 48 | 49 | // The following is needed so that we can support hot reloading our application. 50 | if (process.env.NODE_ENV === 'development' && module.hot) { 51 | // Accept changes to this file for hot reloading. 52 | module.hot.accept('./index.js'); 53 | // Any changes to our routes will cause a hotload re-render. 54 | module.hot.accept( 55 | '../shared/universal/routes', 56 | () => renderApp(require('../shared/universal/routes').default) // eslint-disable-line global-require 57 | ); 58 | } 59 | 60 | renderApp(routes); 61 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | // This grants us source map support, which combined with our webpack source 4 | // maps will give us nice stack traces. 5 | import 'source-map-support/register'; 6 | import path from 'path'; 7 | import appRoot from 'app-root-path'; 8 | import express from 'express'; 9 | import compression from 'compression'; 10 | import hpp from 'hpp'; 11 | import helmet from 'helmet'; 12 | import universalMiddleware from '../universalMiddleware'; 13 | import { notEmpty } from '../shared/universal/utils/guards'; 14 | 15 | const appRootPath = appRoot.toString(); 16 | 17 | // Create our express based server. 18 | const app = express(); 19 | 20 | // Don't expose any software information to hackers. 21 | app.disable('x-powered-by'); 22 | 23 | // Prevent HTTP Parameter pollution. 24 | app.use(hpp()); 25 | 26 | // Content Security Policy 27 | app.use(helmet.contentSecurityPolicy({ 28 | defaultSrc: ["'self'"], 29 | scriptSrc: ["'self'"], 30 | styleSrc: ["'self'"], 31 | imgSrc: ["'self'"], 32 | connectSrc: ["'self'", 'ws:'], 33 | fontSrc: ["'self'"], 34 | objectSrc: ["'none'"], 35 | mediaSrc: ["'none'"], 36 | frameSrc: ["'none'"], 37 | })); 38 | app.use(helmet.xssFilter()); 39 | app.use(helmet.frameguard('deny')); 40 | app.use(helmet.ieNoOpen()); 41 | app.use(helmet.noSniff()); 42 | 43 | // Response compression. 44 | app.use(compression()); 45 | 46 | // Configure static serving of our webpack bundled client files. 47 | app.use( 48 | notEmpty(process.env.CLIENT_BUNDLE_HTTP_PATH), 49 | express.static( 50 | path.resolve(appRootPath, notEmpty(process.env.BUNDLE_OUTPUT_PATH), './client'), 51 | { maxAge: notEmpty(process.env.CLIENT_BUNDLE_CACHE_MAXAGE) } 52 | ) 53 | ); 54 | 55 | // Configure static serving of our "public" root http path static files. 56 | app.use(express.static(path.resolve(appRootPath, './public'))); 57 | 58 | // Bind our universal react app middleware as the handler for all get requests. 59 | if (process.env.NODE_ENV === 'development') { 60 | // In development mode we will use a special wrapper middleware which will 61 | // allow us to flush our node module cache effectively, and it will thereby 62 | // allow us to "hot" reload any builds/updates to our middleware bundle. 63 | const universalDevMiddleware = require('../../tools/development/universalDevMiddleware'); // eslint-disable-line global-require,max-len 64 | 65 | app.get('*', universalDevMiddleware); 66 | } else { 67 | app.get('*', universalMiddleware); 68 | } 69 | 70 | // Create an http listener for our express app. 71 | const port = parseInt(notEmpty(process.env.SERVER_PORT), 10); 72 | const listener = app.listen(port); 73 | console.log(`Server listening on port ${port}`); // eslint-disable-line no-console 74 | 75 | // We export the listener as it will be handy for our development hot reloader. 76 | export default listener; 77 | -------------------------------------------------------------------------------- /src/shared/universal/components/App/App.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import Link from 'react-router/lib/Link'; 5 | import Helmet from 'react-helmet'; 6 | 7 | import 'normalize.css/normalize.css'; 8 | import './globals.css'; 9 | 10 | import Logo from './lib/Logo'; 11 | 12 | const websiteDescription = 13 | 'A starter kit giving you the minimum requirements for a production ready ' + 14 | 'universal react application.'; 15 | 16 | function App(props : { children : $React$Children }) { 17 | const { children } = props; 18 | return ( 19 |
20 | {/* 21 | All of the following will be injected into our page header. 22 | @see https://github.com/nfl/react-helmet 23 | */} 24 | 33 | 34 |
35 | 36 |

React, Universally

37 | {websiteDescription} 38 |
39 |
40 |
    41 |
  • Home
  • 42 |
  • About
  • 43 |
44 |
45 |
46 | {children} 47 |
48 |
49 | ); 50 | } 51 | 52 | export default App; 53 | -------------------------------------------------------------------------------- /src/shared/universal/components/App/globals.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | font-family: Arial, sans-serif; 4 | } 5 | 6 | *, 7 | *:before, 8 | *:after { 9 | box-sizing: border-box; 10 | margin: 0; 11 | padding: 0; 12 | } 13 | -------------------------------------------------------------------------------- /src/shared/universal/components/App/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import App from './App'; 4 | 5 | export default App; 6 | -------------------------------------------------------------------------------- /src/shared/universal/components/App/lib/Logo/Logo.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import logo from './logo.png'; 5 | 6 | function Logo() { 7 | return ( 8 | Logo 9 | ); 10 | } 11 | 12 | export default Logo; 13 | -------------------------------------------------------------------------------- /src/shared/universal/components/App/lib/Logo/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import Logo from './Logo'; 4 | 5 | export default Logo; 6 | -------------------------------------------------------------------------------- /src/shared/universal/components/App/lib/Logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/react-universally-skinny/664841f607f748d1c0c84ba2ee503f09dd9b79cb/src/shared/universal/components/App/lib/Logo/logo.png -------------------------------------------------------------------------------- /src/shared/universal/components/App/views/About/About.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import Helmet from 'react-helmet'; 5 | 6 | function About() { 7 | return ( 8 |
9 | 10 | 11 | Produced with ❤️ 12 | by 13 |   14 | 15 | Sean Matheson 16 | 17 |
18 | ); 19 | } 20 | 21 | export default About; 22 | -------------------------------------------------------------------------------- /src/shared/universal/components/App/views/About/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import About from './About'; 4 | 5 | export default About; 6 | -------------------------------------------------------------------------------- /src/shared/universal/components/App/views/Home/Home.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import Helmet from 'react-helmet'; 5 | 6 | function Home() { 7 | return ( 8 |
9 | 10 | 11 |

12 | This starter kit contains all the build tooling and configuration you 13 | need to kick off your next universal react project, whilst containing a 14 | minimal project set up allowing you to make your own architecture 15 | decisions (redux/mobx etc). 16 |

17 |
18 | ); 19 | } 20 | 21 | export default Home; 22 | -------------------------------------------------------------------------------- /src/shared/universal/components/App/views/Home/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import Home from './Home'; 4 | 5 | export default Home; 6 | -------------------------------------------------------------------------------- /src/shared/universal/routes/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import Route from 'react-router/lib/Route'; 5 | import IndexRoute from 'react-router/lib/IndexRoute'; 6 | import App from '../components/App'; 7 | 8 | /** 9 | * Use this function to provide `getComponent` implementations for your 10 | * application "views" (i.e. pages). It makes use of webpack 2's System.import 11 | * feature which allows for the async module loading behavior, and this results 12 | * in webpack doing code splitting. So essentially we will get a different 13 | * code split bundle for each of our views. Sweet. 14 | * 15 | * Notes: 16 | * 1. Your view components have to reside within the 17 | * ~/src/shared/components/App/views folder. You need to create a folder to 18 | * represent your view and then have an index.js file within that will return 19 | * the respective view component. 20 | * 2. The regex that webpack uses to statically calculate which components it 21 | * should expose as async has been overridden within the config factory. It 22 | * has been overridden so that only the "root" folders within 23 | * ~/src/shared/components/App/views will be recognised as async components. 24 | * None of the sub folders will be considered. 25 | * 26 | * @see https://gist.github.com/sokra/27b24881210b56bbaff7#code-splitting-with-es6 27 | */ 28 | function asyncAppViewResolver(viewName: string) { 29 | const errorHandler = (err) => { 30 | console.log(`==> Failed to load async view "${viewName}".`); // eslint-disable-line no-console 31 | console.log(err); // eslint-disable-line no-console 32 | }; 33 | 34 | return (nextState, cb) => 35 | System.import('../components/App/views/' + viewName + '/index.js') // eslint-disable-line prefer-template 36 | .then(module => cb(null, module.default)) 37 | .catch(errorHandler); 38 | } 39 | 40 | /** 41 | * Our routes. 42 | * 43 | * Note: We load our routes asynhronously using the `getComponent` API of 44 | * react-router, doing so combined with the `System.import` support by 45 | * webpack 2 allows us to get code splitting based on our routes. 46 | * 47 | * @see https://github.com/reactjs/react-router/blob/master/docs/guides/DynamicRouting.md 48 | */ 49 | const routes = ( 50 | 51 | 52 | 53 | 54 | ); 55 | 56 | export default routes; 57 | -------------------------------------------------------------------------------- /src/shared/universal/utils/guards.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* eslint-disable import/prefer-default-export */ 3 | 4 | export function notEmpty(x: ?T, message?: string) : T { 5 | if (x == null) { 6 | throw new Error(message || 'Expected value to not be empty.'); 7 | } 8 | 9 | return x; 10 | } 11 | -------------------------------------------------------------------------------- /src/universalMiddleware/clientAssets.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | // This file resolves the assets available from our client bundle. 4 | 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | import appRoot from 'app-root-path'; 8 | import { notEmpty } from '../shared/universal/utils/guards'; 9 | 10 | const appRootPath = appRoot.toString(); 11 | 12 | const assetsBundleFilePath = path.resolve( 13 | appRootPath, 14 | notEmpty(process.env.BUNDLE_OUTPUT_PATH), 15 | './client', 16 | `./${notEmpty(process.env.BUNDLE_ASSETS_FILENAME)}` 17 | ); 18 | 19 | if (!fs.existsSync(assetsBundleFilePath)) { 20 | throw new Error( 21 | `We could not find the "${assetsBundleFilePath}" file, which contains a ` + 22 | 'list of the assets of the client bundle. Please ensure that the client ' + 23 | 'bundle has been built before the server bundle and that the required ' + 24 | 'environment variables are configured (BUNDLE_OUTPUT_PATH & ' + 25 | 'BUNDLE_ASSETS_FILENAME)' 26 | ); 27 | } 28 | 29 | const assetsBundleFileContents = JSON.parse( 30 | fs.readFileSync(assetsBundleFilePath, 'utf8') 31 | ); 32 | 33 | // Convert the assets json it into an object that contains all the paths to our 34 | // javascript and css files. Doing this is required as for production 35 | // configurations we add a hash to our filenames, therefore we won't know the 36 | // paths of the output by webpack unless we read them from the assets.json file. 37 | const assets = Object.keys(assetsBundleFileContents) 38 | .map(key => assetsBundleFileContents[key]) 39 | .reduce((acc, chunk) => { 40 | if (chunk.js) { 41 | acc.scripts.push(chunk.js); 42 | } 43 | if (chunk.css) { 44 | acc.styles.push(chunk.css); 45 | } 46 | return acc; 47 | }, { scripts: [], styles: [] }); 48 | 49 | export default assets; 50 | -------------------------------------------------------------------------------- /src/universalMiddleware/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type { $Request, $Response, Middleware } from 'express'; 4 | import React from 'react'; 5 | import RouterContext from 'react-router/lib/RouterContext'; 6 | import createMemoryHistory from 'react-router/lib/createMemoryHistory'; 7 | import match from 'react-router/lib/match'; 8 | import render from './render'; 9 | import routes from '../shared/universal/routes'; 10 | 11 | /** 12 | * An express middleware that is capabable of doing React server side rendering. 13 | */ 14 | function universalReactAppMiddleware(request: $Request, response: $Response) { 15 | if (process.env.DISABLE_SSR === 'true') { 16 | if (process.env.NODE_ENV === 'development') { 17 | console.log('==> Handling react route without SSR'); // eslint-disable-line no-console 18 | } 19 | // SSR is disabled so we will just return an empty html page and will 20 | // rely on the client to populate the initial react application state. 21 | const html = render(); 22 | response.status(200).send(html); 23 | return; 24 | } 25 | 26 | const history = createMemoryHistory(request.originalUrl); 27 | 28 | // Server side handling of react-router. 29 | // Read more about this here: 30 | // https://github.com/reactjs/react-router/blob/master/docs/guides/ServerRendering.md 31 | match({ routes, history }, (error, redirectLocation, renderProps) => { 32 | if (error) { 33 | response.status(500).send(error.message); 34 | } else if (redirectLocation) { 35 | response.redirect(302, redirectLocation.pathname + redirectLocation.search); 36 | } else if (renderProps) { 37 | // You can check renderProps.components or renderProps.routes for 38 | // your "not found" component or route respectively, and send a 404 as 39 | // below, if you're using a catch-all route. 40 | 41 | const html = render(); 42 | response.status(200).send(html); 43 | } else { 44 | response.status(404).send('Not found'); 45 | } 46 | }); 47 | } 48 | 49 | export default (universalReactAppMiddleware : Middleware); 50 | -------------------------------------------------------------------------------- /src/universalMiddleware/render.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { renderToString } from 'react-dom/server'; 4 | import serialize from 'serialize-javascript'; 5 | import Helmet from 'react-helmet'; 6 | import clientAssets from './clientAssets'; 7 | 8 | function styleTags(styles : Array) { 9 | return styles 10 | .map(style => 11 | `` 12 | ) 13 | .join('\n'); 14 | } 15 | 16 | function scriptTags(scripts : Array) { 17 | return scripts 18 | .map(script => 19 | `` 20 | ) 21 | .join('\n'); 22 | } 23 | 24 | const styles = styleTags(clientAssets.styles); 25 | const scripts = scriptTags(clientAssets.scripts); 26 | 27 | /** 28 | * Generates a full HTML page containing the render output of the given react 29 | * element. 30 | * 31 | * @param rootReactElement 32 | * [Optional] The root React element to be rendered on the page. 33 | * @param initialState 34 | * [Optional] The initial state for the redux store which will be used by the 35 | * client to mount the redux store into the desired state. 36 | * 37 | * @return The full HTML page in the form of a React element. 38 | */ 39 | function render(rootReactElement : ?$React$Element, initialState : ?Object) { 40 | const reactRenderString = rootReactElement 41 | ? renderToString(rootReactElement) 42 | : null; 43 | 44 | const helmet = rootReactElement 45 | // We run 'react-helmet' after our renderToString call so that we can fish 46 | // out all the attributes which need to be attached to our page. 47 | // React Helmet allows us to control our page header contents via our 48 | // components. 49 | // @see https://github.com/nfl/react-helmet 50 | ? Helmet.rewind() 51 | // There was no react element, so we just us an empty helmet. 52 | : null; 53 | 54 | return ` 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | ${helmet ? helmet.title.toString() : ''} 63 | ${helmet ? helmet.meta.toString() : ''} 64 | ${helmet ? helmet.link.toString() : ''} 65 | 66 | ${styles} 67 | ${helmet ? helmet.style.toString() : ''} 68 | 69 | 70 | 71 | 72 |
${reactRenderString || ''}
73 | 74 | 79 | 80 | ${scripts} 81 | ${helmet ? helmet.script.toString() : ''} 82 | 83 | `; 84 | } 85 | 86 | export default render; 87 | -------------------------------------------------------------------------------- /tools/config/envVars.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const dotenv = require('dotenv'); 6 | const appRoot = require('app-root-path'); 7 | 8 | const appRootPath = appRoot.toString(); 9 | 10 | // This is to support deployment to the "now" host. See the README for more info. 11 | const envFile = process.env.NOW 12 | ? './.envnow' 13 | : './.env'; 14 | 15 | // @see https://github.com/motdotla/dotenv 16 | const envVars = dotenv.parse( 17 | fs.readFileSync(path.resolve(appRootPath, envFile), 'utf8') 18 | ); 19 | 20 | module.exports = envVars; 21 | -------------------------------------------------------------------------------- /tools/development/createNotification.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable global-require */ 3 | /* eslint-disable no-underscore-dangle */ 4 | /* eslint-disable import/no-extraneous-dependencies */ 5 | 6 | const notifier = require('node-notifier'); 7 | const colors = require('colors'); 8 | 9 | function createNotification(options = {}) { 10 | const title = options.title 11 | ? `${options.title.toUpperCase()}` 12 | : undefined; 13 | 14 | notifier.notify({ 15 | title, 16 | message: options.message, 17 | open: options.open, 18 | }); 19 | 20 | const level = options.level || 'info'; 21 | const msg = `==> ${title} -> ${options.message}`; 22 | 23 | switch (level) { 24 | case 'warn': console.log(colors.red(msg)); break; 25 | case 'error': console.log(colors.bgRed.white(msg)); break; 26 | case 'info': 27 | default: console.log(colors.green(msg)); 28 | } 29 | } 30 | 31 | module.exports = createNotification; 32 | -------------------------------------------------------------------------------- /tools/development/hotClient.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable global-require */ 3 | /* eslint-disable no-underscore-dangle */ 4 | /* eslint-disable import/no-extraneous-dependencies */ 5 | 6 | const express = require('express'); 7 | const createWebpackMiddleware = require('webpack-dev-middleware'); 8 | const createWebpackHotMiddleware = require('webpack-hot-middleware'); 9 | const envVars = require('../config/envVars'); 10 | const ListenerManager = require('./listenerManager'); 11 | const createNotification = require('./createNotification'); 12 | 13 | class HotClient { 14 | constructor(compiler) { 15 | const app = express(); 16 | 17 | this.webpackDevMiddleware = createWebpackMiddleware(compiler, { 18 | quiet: true, 19 | noInfo: true, 20 | headers: { 21 | 'Access-Control-Allow-Origin': '*', 22 | }, 23 | // The path at which the client bundles are served from. Note: in this 24 | // case as we are running a seperate dev server the public path should 25 | // be absolute, i.e. including the "http://..." 26 | publicPath: compiler.options.output.publicPath, 27 | }); 28 | 29 | app.use(this.webpackDevMiddleware); 30 | app.use(createWebpackHotMiddleware(compiler)); 31 | 32 | const listener = app.listen(envVars.CLIENT_DEVSERVER_PORT); 33 | 34 | this.listenerManager = new ListenerManager(listener, 'client'); 35 | 36 | compiler.plugin('compile', () => { 37 | createNotification({ 38 | title: 'client', 39 | level: 'info', 40 | message: 'Building new bundle...', 41 | }); 42 | }); 43 | 44 | compiler.plugin('done', (stats) => { 45 | if (stats.hasErrors()) { 46 | createNotification({ 47 | title: 'client', 48 | level: 'error', 49 | message: 'Build failed, please check the console for more information.', 50 | }); 51 | console.log(stats.toString()); 52 | } else { 53 | createNotification({ 54 | title: 'client', 55 | level: 'info', 56 | message: 'Running with latest changes.', 57 | }); 58 | } 59 | }); 60 | } 61 | 62 | dispose() { 63 | this.webpackDevMiddleware.close(); 64 | 65 | return this.listenerManager 66 | ? this.listenerManager.dispose() 67 | : Promise.resolve(); 68 | } 69 | } 70 | 71 | module.exports = HotClient; 72 | -------------------------------------------------------------------------------- /tools/development/hotServer.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable global-require */ 3 | /* eslint-disable no-underscore-dangle */ 4 | /* eslint-disable import/no-extraneous-dependencies */ 5 | /* eslint-disable import/no-dynamic-require */ 6 | 7 | const path = require('path'); 8 | const chokidar = require('chokidar'); 9 | const envVars = require('../config/envVars'); 10 | const ListenerManager = require('./listenerManager'); 11 | const createNotification = require('./createNotification'); 12 | 13 | class HotServer { 14 | constructor(compiler) { 15 | this.listenerManager = null; 16 | 17 | const runCompiler = () => { 18 | createNotification({ 19 | title: 'server', 20 | level: 'info', 21 | message: 'Building new server bundle...', 22 | }); 23 | 24 | compiler.run(() => undefined); 25 | }; 26 | 27 | const compiledOutputPath = path.resolve( 28 | compiler.options.output.path, `${Object.keys(compiler.options.entry)[0]}.js` 29 | ); 30 | 31 | compiler.plugin('done', (stats) => { 32 | if (stats.hasErrors()) { 33 | createNotification({ 34 | title: 'server', 35 | level: 'error', 36 | message: 'Build failed, check the console for more information.', 37 | }); 38 | console.log(stats.toString()); 39 | return; 40 | } 41 | 42 | // Make sure our newly built server bundles aren't in the module cache. 43 | Object.keys(require.cache).forEach((modulePath) => { 44 | if (modulePath.indexOf(compiler.options.output.path) !== -1) { 45 | delete require.cache[modulePath]; 46 | } 47 | }); 48 | 49 | try { 50 | // The server bundle will automatically start the web server just by 51 | // requiring it. It returns the http listener too. 52 | const listener = require(compiledOutputPath).default; 53 | this.listenerManager = new ListenerManager(listener, 'server'); 54 | 55 | const url = `http://localhost:${envVars.SERVER_PORT}`; 56 | 57 | createNotification({ 58 | title: 'server', 59 | level: 'info', 60 | message: `Running on ${url} with latest changes.`, 61 | open: url, 62 | }); 63 | } catch (err) { 64 | createNotification({ 65 | title: 'server', 66 | level: 'error', 67 | message: 'Failed to start, please check the console for more information.', 68 | }); 69 | console.log(err); 70 | } 71 | }); 72 | 73 | // Now we will configure `chokidar` to watch our server specific source folder. 74 | // Any changes will cause a rebuild of the server bundle. 75 | 76 | const compileHotServer = () => { 77 | // Shut down any existing running listener if necessary before starting the 78 | // compile, else just compile. 79 | if (this.listenerManager) { 80 | this.listenerManager.dispose(true).then(runCompiler); 81 | } else { 82 | runCompiler(); 83 | } 84 | }; 85 | 86 | this.watcher = chokidar.watch([path.resolve(__dirname, '../../src/server')]); 87 | this.watcher.on('ready', () => { 88 | this.watcher 89 | .on('add', compileHotServer) 90 | .on('addDir', compileHotServer) 91 | .on('change', compileHotServer) 92 | .on('unlink', compileHotServer) 93 | .on('unlinkDir', compileHotServer); 94 | }); 95 | 96 | // Lets start the compile. 97 | runCompiler(); 98 | } 99 | 100 | dispose() { 101 | return this.listenerManager 102 | ? this.listenerManager.dispose() 103 | : Promise.resolve(); 104 | } 105 | } 106 | 107 | module.exports = HotServer; 108 | -------------------------------------------------------------------------------- /tools/development/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable global-require */ 3 | /* eslint-disable no-underscore-dangle */ 4 | /* eslint-disable import/no-extraneous-dependencies */ 5 | /* eslint-disable import/newline-after-import */ 6 | 7 | const path = require('path'); 8 | const chokidar = require('chokidar'); 9 | const webpack = require('webpack'); 10 | const createNotification = require('./createNotification'); 11 | const HotServer = require('./hotServer'); 12 | const HotClient = require('./hotClient'); 13 | 14 | class HotDevelopment { 15 | constructor() { 16 | try { 17 | const clientConfigFactory = require('../webpack/client.config'); 18 | const clientConfig = clientConfigFactory({ mode: 'development' }); 19 | this.clientCompiler = webpack(clientConfig); 20 | 21 | const middlewareConfigFactory = require('../webpack/universalMiddleware.config'); 22 | const middlewareConfig = middlewareConfigFactory({ mode: 'development' }); 23 | this.middlewareCompiler = webpack(middlewareConfig); 24 | 25 | const serverConfigFactory = require('../webpack/server.config'); 26 | const serverConfig = serverConfigFactory({ mode: 'development' }); 27 | this.serverCompiler = webpack(serverConfig); 28 | } catch (err) { 29 | createNotification({ 30 | title: 'development', 31 | level: 'error', 32 | message: 'Webpack configs are invalid, please check the console for more information.', 33 | }); 34 | console.log(err); 35 | return; 36 | } 37 | 38 | this.prepHotServer(); 39 | this.prepHotUniversalMiddleware(); 40 | this.prepHotClient(); 41 | } 42 | 43 | prepHotClient() { 44 | this.clientBundle = new HotClient(this.clientCompiler); 45 | } 46 | 47 | prepHotUniversalMiddleware() { 48 | let started = false; 49 | 50 | const runMiddlewareCompiler = () => { 51 | this.middlewareCompiler.watch({}, () => undefined); 52 | }; 53 | 54 | this.clientCompiler.plugin('done', (stats) => { 55 | if (!stats.hasErrors() && !started) { 56 | started = true; 57 | runMiddlewareCompiler(); 58 | } 59 | }); 60 | 61 | this.middlewareCompiler.plugin('compile', () => { 62 | createNotification({ 63 | title: 'universalMiddleware', 64 | level: 'info', 65 | message: 'Building new bundle...', 66 | }); 67 | }); 68 | 69 | this.middlewareCompiler.plugin('done', (stats) => { 70 | if (!stats.hasErrors()) { 71 | // Make sure our newly built bundle is removed from the module cache. 72 | Object.keys(require.cache).forEach((modulePath) => { 73 | if (modulePath.indexOf('universalMiddleware') !== -1) { 74 | delete require.cache[modulePath]; 75 | } 76 | }); 77 | 78 | createNotification({ 79 | title: 'universalMiddleware', 80 | level: 'info', 81 | message: 'Available with latest changes.', 82 | }); 83 | } else { 84 | createNotification({ 85 | title: 'universalMiddleware', 86 | level: 'error', 87 | message: 'Build failed, please check the console for more information.', 88 | }); 89 | console.log(stats.toString()); 90 | } 91 | }); 92 | } 93 | 94 | prepHotServer() { 95 | let clientBuilt = false; 96 | let middlewareBuilt = false; 97 | let started = false; 98 | 99 | const startServerBundleWhenReady = () => { 100 | if (!started && (clientBuilt && middlewareBuilt)) { 101 | started = true; 102 | this.serverBundle = new HotServer(this.serverCompiler); 103 | } 104 | }; 105 | 106 | this.clientCompiler.plugin('done', (stats) => { 107 | if (!stats.hasErrors() && !clientBuilt) { 108 | clientBuilt = true; 109 | startServerBundleWhenReady(); 110 | } 111 | }); 112 | 113 | this.middlewareCompiler.plugin('done', (stats) => { 114 | if (!stats.hasErrors() && !middlewareBuilt) { 115 | middlewareBuilt = true; 116 | startServerBundleWhenReady(); 117 | } 118 | }); 119 | } 120 | 121 | dispose() { 122 | const safeDisposer = bundle => () => (bundle ? bundle.dispose() : Promise.resolve()); 123 | const safeDisposeClient = safeDisposer(this.clientBundle); 124 | const safeDisposeServer = safeDisposer(this.serverBundle); 125 | 126 | return safeDisposeClient() 127 | .then(() => console.log('disposed client')) 128 | .then(safeDisposeServer); 129 | } 130 | } 131 | 132 | const hotDevelopment = new HotDevelopment(); 133 | 134 | // Any changes to our webpack configs should be notified as requiring a restart 135 | // of the development tool. 136 | const watcher = chokidar.watch( 137 | path.resolve(__dirname, '../webpack') 138 | ); 139 | watcher.on('ready', () => { 140 | watcher.on('change', () => { 141 | createNotification({ 142 | title: 'webpack', 143 | level: 'warn', 144 | message: 'Webpack config changed. Please restart your development server to use the latest version of the configs.', 145 | }); 146 | }); 147 | }); 148 | 149 | // If we receive a kill cmd then we will first try to dispose our listeners. 150 | process.on('SIGTERM', () => hotDevelopment.dispose().then(() => process.exit(0))); 151 | -------------------------------------------------------------------------------- /tools/development/listenerManager.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable global-require */ 3 | /* eslint-disable no-underscore-dangle */ 4 | /* eslint-disable import/no-extraneous-dependencies */ 5 | 6 | const createNotification = require('./createNotification'); 7 | 8 | class ListenerManager { 9 | constructor(listener, name) { 10 | this.name = name || 'listener'; 11 | this.lastConnectionKey = 0; 12 | this.connectionMap = {}; 13 | this.listener = listener; 14 | 15 | // Track all connections to our server so that we can close them when needed. 16 | this.listener.on('connection', (connection) => { 17 | // Generate a new key to represent the connection 18 | const connectionKey = this.lastConnectionKey + 1; 19 | // Add the connection to our map. 20 | this.connectionMap[connectionKey] = connection; 21 | // Remove the connection from our map when it closes. 22 | connection.on('close', () => { 23 | delete this.connectionMap[connectionKey]; 24 | }); 25 | }); 26 | } 27 | 28 | killAllConnections() { 29 | Object.keys(this.connectionMap).forEach((connectionKey) => { 30 | this.connectionMap[connectionKey].destroy(); 31 | }); 32 | } 33 | 34 | dispose() { 35 | return new Promise((resolve) => { 36 | if (this.listener) { 37 | this.killAllConnections(); 38 | 39 | createNotification({ 40 | title: this.name, 41 | level: 'info', 42 | message: 'Destroyed all existing connections.', 43 | }); 44 | 45 | this.listener.close(() => { 46 | this.killAllConnections(); 47 | 48 | createNotification({ 49 | title: this.name, 50 | level: 'info', 51 | message: 'Closed listener.', 52 | }); 53 | }); 54 | 55 | resolve(); 56 | } else { 57 | resolve(); 58 | } 59 | }); 60 | } 61 | } 62 | 63 | module.exports = ListenerManager; 64 | -------------------------------------------------------------------------------- /tools/development/universalDevMiddleware.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | 3 | // We have to use this wrapper for our universalMiddleware in development mode 4 | // as webpack has it's own module require/resolver system which can get in the 5 | // way when we try to flush our node js module cache in the hope that our 6 | // server will then use the newly built version of our middleware bundle. 7 | // Having this wrapped version allows us to "dodge" webpack taking over the 8 | // module resolution. 9 | const universalDevMiddleware = (req, resp) => { 10 | const wrappedMiddleware = require('../../build/universalMiddleware/index.js').default; 11 | 12 | wrappedMiddleware(req, resp); 13 | }; 14 | 15 | module.exports = universalDevMiddleware; 16 | -------------------------------------------------------------------------------- /tools/flow/definitions/commonjs.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | type WebpackHot = { 4 | accept(path: ?string) : void; 5 | } 6 | 7 | declare var module: { 8 | exports: any; 9 | require(id: string): any; 10 | id: string; 11 | filename: string; 12 | loaded: boolean; 13 | parent: any; 14 | children: Array; 15 | // We extend the common js format with the following to allow for the 16 | // webpack related feature. 17 | hot: WebpackHot 18 | }; 19 | -------------------------------------------------------------------------------- /tools/flow/definitions/es6modules.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | declare var System: { 4 | import(path: string): any; 5 | }; 6 | -------------------------------------------------------------------------------- /tools/flow/definitions/react.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | // Note: we already have the definitions from 4 | // https://github.com/facebook/flow/blob/master/lib/react.js 5 | // so the below are merely helpful extensions. 6 | 7 | import React from 'react'; 8 | 9 | declare type $React$Element = React.Element<*>; 10 | 11 | declare type $React$Node = string | number | $React$Element | Array<$React$Element>; 12 | 13 | declare type $React$Child = $React$Node | boolean | void | null; 14 | 15 | declare type $React$Children = $React$Child | Array<$React$Children>; 16 | -------------------------------------------------------------------------------- /tools/flow/flow-typed/README.md: -------------------------------------------------------------------------------- 1 | These definitions come from the "flow-typed" repo. 2 | 3 | Install/upgrade them by opening the `/tools/flow` directory in your console and then executing the `flow-typed` cli. 4 | -------------------------------------------------------------------------------- /tools/flow/flow-typed/npm/express_v4.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: ce6eddb651fe9035366f36c446364736 2 | // flow-typed version: 08386ddd61/express_v4.x.x/flow_>=v0.25.x 3 | 4 | // @flow 5 | import type { Server } from 'http'; 6 | declare module 'express' { 7 | declare type RouterOptions = { 8 | caseSensitive?: boolean, 9 | mergeParams?: boolean, 10 | strict?: boolean 11 | }; 12 | declare class RequestResponseBase { 13 | app: Application; 14 | get(field: string): string | void; 15 | } 16 | declare class Request extends http$IncomingMessage mixins RequestResponseBase { 17 | baseUrl: string; 18 | body: mixed; 19 | cookies: {[cookie: string]: string}; 20 | fresh: boolean; 21 | hostname: boolean; 22 | ip: string; 23 | ips: Array; 24 | method: string; 25 | originalUrl: string; 26 | params: {[param: string]: string}; 27 | path: string; 28 | protocol: 'https' | 'http'; 29 | query: {[name: string]: string}; 30 | route: string; 31 | secure: boolean; 32 | signedCookies: {[signedCookie: string]: string}; 33 | stale: boolean; 34 | subdomains: Array; 35 | xhr: boolean; 36 | accepts(types: string): string | false; 37 | acceptsCharsets(...charsets: Array): string | false; 38 | acceptsEncodings(...encoding: Array): string | false; 39 | acceptsLanguages(...lang: Array): string | false; 40 | header(field: string): string | void; 41 | is(type: string): boolean; 42 | param(name: string, defaultValue?: string): string | void; 43 | } 44 | 45 | 46 | declare type CookieOptions = { 47 | domain?: string, 48 | encode?: (value: string) => string, 49 | expires?: Date, 50 | httpOnly?: boolean, 51 | maxAge?: string, 52 | path?: string, 53 | secure?: boolean, 54 | signed?: boolean 55 | }; 56 | 57 | declare type RenderCallback = (err: Error | null, html?: string) => mixed; 58 | 59 | declare type SendFileOptions = { 60 | maxAge?: number, 61 | root?: string, 62 | lastModified?: boolean, 63 | headers?: {[name: string]: string}, 64 | dotfiles?: 'allow' | 'deny' | 'ignore' 65 | }; 66 | declare class Response extends http$ClientRequest mixins RequestResponseBase { 67 | headersSent: boolean; 68 | locals: {[name: string]: mixed}; 69 | append(field: string, value?: string): this; 70 | attachment(filename?: string): this; 71 | cookie(name: string, value: string, options?: CookieOptions): this; 72 | clearCookie(name: string, options?: CookieOptions): this; 73 | download(path: string, filename?: string, callback?: (err?: ?Error) => void): this; 74 | format(typesObject: {[type: string]: Function}): this; 75 | json(body?: mixed): this; 76 | jsonp(body?: mixed): this; 77 | links(links: {[name: string]: string}): this; 78 | location(path: string): this; 79 | redirect(url: string, ...args: Array): this; 80 | redirect(status: number, url: string, ...args: Array): this; 81 | render(view: string, locals?: mixed, callback: RenderCallback): this; 82 | send(body?: mixed): this; 83 | sendFile(path: string, options?: SendFileOptions, callback?: (err?: ?Error) => mixed): this; 84 | sendStatus(statusCode: number): this; 85 | set(field: string, value?: string): this; 86 | status(statusCode: number): this; 87 | type(type: string): this; 88 | vary(field: string): this; 89 | } 90 | declare type $Response = Response; 91 | declare type $Request = Request; 92 | declare type NextFunction = (err?: ?Error) => mixed; 93 | declare type Middleware = 94 | ((req: Request, res: Response, next: NextFunction) => mixed) | 95 | ((error: ?Error, req : Request, res: Response, next: NextFunction) => mixed); 96 | declare interface RouteMethodType { 97 | (middleware: Middleware): T; 98 | (...middleware: Array): T; 99 | (path: string|RegExp|string[], ...middleware: Array): T; 100 | } 101 | declare interface RouterMethodType { 102 | (middleware: Middleware): T; 103 | (...middleware: Array): T; 104 | (path: string|RegExp|string[], ...middleware: Array): T; 105 | (path: string, router: Router): T; 106 | } 107 | declare class Route { 108 | all: RouteMethodType; 109 | get: RouteMethodType; 110 | post: RouteMethodType; 111 | put: RouteMethodType; 112 | head: RouteMethodType; 113 | delete: RouteMethodType; 114 | options: RouteMethodType; 115 | trace: RouteMethodType; 116 | copy: RouteMethodType; 117 | lock: RouteMethodType; 118 | mkcol: RouteMethodType; 119 | move: RouteMethodType; 120 | purge: RouteMethodType; 121 | propfind: RouteMethodType; 122 | proppatch: RouteMethodType; 123 | unlock: RouteMethodType; 124 | report: RouteMethodType; 125 | mkactivity: RouteMethodType; 126 | checkout: RouteMethodType; 127 | merge: RouteMethodType; 128 | 129 | // @TODO Missing 'm-search' but get flow illegal name error. 130 | 131 | notify: RouteMethodType; 132 | subscribe: RouteMethodType; 133 | unsubscribe: RouteMethodType; 134 | patch: RouteMethodType; 135 | search: RouteMethodType; 136 | connect: RouteMethodType; 137 | } 138 | declare class Router extends Route { 139 | constructor(options?: RouterOptions): void; 140 | 141 | use: RouterMethodType; 142 | 143 | route(path: string): Route; 144 | 145 | static (): Router; 146 | } 147 | 148 | declare function serveStatic(root: string, options?: Object): Middleware; 149 | 150 | declare class Application extends Router mixins events$EventEmitter { 151 | constructor(): void; 152 | locals: {[name: string]: mixed}; 153 | mountpath: string; 154 | listen(port: number, hostname?: string, backlog?: number, callback?: (err?: ?Error) => mixed): Server; 155 | listen(port: number, hostname?: string, callback?: (err?: ?Error) => mixed): Server; 156 | listen(port: number, callback?: (err?: ?Error) => mixed): Server; 157 | listen(path: string, callback?: (err?: ?Error) => mixed): Server; 158 | listen(handle: Object, callback?: (err?: ?Error) => mixed): Server; 159 | disable(name: string): void; 160 | disabled(name: string): boolean; 161 | enable(name: string): void; 162 | enabled(name: string): boolean; 163 | engine(name: string, callback: Function): void; 164 | /** 165 | * Mixed will not be taken as a value option. Issue around using the GET http method name and the get for settings. 166 | */ 167 | // get(name: string): mixed; 168 | set(name: string, value: mixed): mixed; 169 | render(name: string, optionsOrFunction: {[name: string]: mixed}, callback: RenderCallback): void; 170 | } 171 | 172 | declare type $Application = Application; 173 | 174 | declare module.exports: { 175 | (): Application, // If you try to call like a function, it will use this signature 176 | static: serveStatic, // `static` property on the function 177 | Router: typeof Router, // `Router` property on the function 178 | }; 179 | } 180 | -------------------------------------------------------------------------------- /tools/flow/stubs/WebpackAsset.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare export default string 4 | -------------------------------------------------------------------------------- /tools/utils/index.js: -------------------------------------------------------------------------------- 1 | // :: [Any] -> [Any] 2 | function removeEmpty(x) { 3 | return x.filter(y => !!y); 4 | } 5 | 6 | // :: bool -> (Any, Any) -> Any 7 | function ifElse(condition) { 8 | return (then, or) => (condition ? then : or); 9 | } 10 | 11 | // :: ...Object -> Object 12 | function merge() { 13 | const funcArgs = Array.prototype.slice.call(arguments); // eslint-disable-line prefer-rest-params 14 | 15 | return Object.assign.apply( 16 | null, 17 | removeEmpty([{}].concat(funcArgs)) 18 | ); 19 | } 20 | 21 | module.exports = { 22 | removeEmpty, 23 | ifElse, 24 | merge, 25 | }; 26 | -------------------------------------------------------------------------------- /tools/webpack/client.config.js: -------------------------------------------------------------------------------- 1 | const webpackConfigFactory = require('./configFactory'); 2 | 3 | module.exports = function clientConfigFactory(options = {}, args = {}) { 4 | const { mode = 'production' } = options; 5 | return webpackConfigFactory({ target: 'client', mode }, args); 6 | }; 7 | -------------------------------------------------------------------------------- /tools/webpack/configFactory.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console,import/no-extraneous-dependencies */ 2 | 3 | const path = require('path'); 4 | const webpack = require('webpack'); 5 | const AssetsPlugin = require('assets-webpack-plugin'); 6 | const nodeExternals = require('webpack-node-externals'); 7 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 8 | const appRoot = require('app-root-path'); 9 | const WebpackMd5Hash = require('webpack-md5-hash'); 10 | const { removeEmpty, ifElse, merge } = require('../utils'); 11 | const envVars = require('../config/envVars'); 12 | 13 | const appRootPath = appRoot.toString(); 14 | 15 | function webpackConfigFactory({ target, mode }, { json }) { 16 | if (!target || ['client', 'server', 'universalMiddleware'].findIndex(valid => target === valid) === -1) { 17 | throw new Error( 18 | 'You must provide a "target" (client|server|universalMiddleware) to the webpackConfigFactory.' 19 | ); 20 | } 21 | 22 | if (!mode || ['development', 'production'].findIndex(valid => mode === valid) === -1) { 23 | throw new Error( 24 | 'You must provide a "mode" (development|production) to the webpackConfigFactory.' 25 | ); 26 | } 27 | 28 | if (!json) { 29 | // Our bundle is outputing json for bundle analysis, therefore we don't 30 | // want to do this console output as it will interfere with the json output. 31 | // 32 | // You can run a bundle analysis by executing the following: 33 | // 34 | // $(npm bin)/webpack \ 35 | // --env.mode production \ 36 | // --config webpack.client.config.js \ 37 | // --json \ 38 | // --profile \ 39 | // > build/client/analysis.json 40 | // 41 | // And then upload the build/client/analysis.json to http://webpack.github.io/analyse/ 42 | // This allows you to analyse your webpack bundle to make sure it is 43 | // optimal. 44 | console.log(`==> Creating webpack config for "${target}" in "${mode}" mode`); 45 | } 46 | 47 | const isDev = mode === 'development'; 48 | const isProd = mode === 'production'; 49 | const isClient = target === 'client'; 50 | const isServer = target === 'server'; 51 | const isUniversalMiddleware = target === 'universalMiddleware'; 52 | const isNodeTarget = isServer || isUniversalMiddleware; 53 | 54 | const ifNodeTarget = ifElse(isNodeTarget); 55 | const ifReactTarget = ifElse(isClient || isUniversalMiddleware); 56 | const ifDev = ifElse(isDev); 57 | const ifClient = ifElse(isClient); 58 | const ifServer = ifElse(isServer); 59 | const ifDevServer = ifElse(isDev && isServer); 60 | const ifDevClient = ifElse(isDev && isClient); 61 | const ifProdClient = ifElse(isProd && isClient); 62 | 63 | return { 64 | // We need to state that we are targetting "node" for our server bundle. 65 | target: ifNodeTarget('node', 'web'), 66 | // We have to set this to be able to use these items when executing a 67 | // server bundle. Otherwise strangeness happens, like __dirname resolving 68 | // to '/'. There is no effect on our client bundle. 69 | node: { 70 | __dirname: true, 71 | __filename: true, 72 | }, 73 | // Anything listed in externals will not be included in our bundle. 74 | externals: removeEmpty([ 75 | // Don't allow the server to bundle the universal middleware bundle. We 76 | // want the server to natively require it from the build dir. 77 | ifServer(/universalMiddleware/), 78 | ifDevServer(/universalDevMiddleware/), 79 | 80 | // We don't want our node_modules to be bundled with our server package, 81 | // prefering them to be resolved via native node module system. Therefore 82 | // we use the `webpack-node-externals` library to help us generate an 83 | // externals config that will ignore all node_modules. 84 | ifNodeTarget(nodeExternals({ 85 | // NOTE: !!! 86 | // However the node_modules may contain files that will rely on our 87 | // webpack loaders in order to be used/resolved, for example CSS or 88 | // SASS. For these cases please make sure that the file extensions 89 | // are added to the below list. We have added the most common formats. 90 | whitelist: [ 91 | /\.(eot|woff|woff2|ttf|otf)$/, 92 | /\.(svg|png|jpg|jpeg|gif|ico)$/, 93 | /\.(mp4|mp3|ogg|swf|webp)$/, 94 | /\.(css|scss|sass|sss|less)$/, 95 | 96 | // We need to add any react modules to our whitelist as we need 97 | // webpack to alias any imports of react/react-dom to the respective 98 | // preact libraries. 99 | /react/, 100 | ], 101 | })), 102 | ]), 103 | devtool: ifElse(isNodeTarget || isDev)( 104 | // We want to be able to get nice stack traces when running our server 105 | // bundle. To fully support this we'll also need to configure the 106 | // `node-source-map-support` module to execute at the start of the server 107 | // bundle. This module will allow the node to make use of the 108 | // source maps. 109 | // We also want to be able to link to the source in chrome dev tools 110 | // whilst we are in development mode. :) 111 | 'source-map', 112 | // When in production client mode we don't want any source maps to 113 | // decrease our payload sizes. 114 | // This form has almost no cost. 115 | 'hidden-source-map' 116 | ), 117 | // Define our entry chunks for our bundle. 118 | entry: merge( 119 | { 120 | index: removeEmpty([ 121 | ifDevClient(`webpack-hot-middleware/client?reload=true&path=http://localhost:${envVars.CLIENT_DEVSERVER_PORT}/__webpack_hmr`), 122 | // We are using polyfill.io instead of the very heavy babel-polyfill. 123 | // Therefore we need to add the regenerator-runtime as the babel-polyfill 124 | // included this, which polyfill.io doesn't include. 125 | ifClient('regenerator-runtime/runtime'), 126 | path.resolve(appRootPath, `./src/${target}/index.js`), 127 | ]), 128 | } 129 | ), 130 | output: { 131 | // The dir in which our bundle should be output. 132 | path: path.resolve(appRootPath, envVars.BUNDLE_OUTPUT_PATH, `./${target}`), 133 | // The filename format for our bundle's entries. 134 | filename: ifProdClient( 135 | // We include a hash for client caching purposes. Including a unique 136 | // has for every build will ensure browsers always fetch our newest 137 | // bundle. 138 | '[name]-[chunkhash].js', 139 | // We want a determinable file name when running our server bundles, 140 | // as we need to be able to target our server start file from our 141 | // npm scripts. We don't care about caching on the server anyway. 142 | // We also want our client development builds to have a determinable 143 | // name for our hot reloading client bundle server. 144 | '[name].js' 145 | ), 146 | chunkFilename: '[name]-[chunkhash].js', 147 | // This is the web path under which our webpack bundled output should 148 | // be considered as being served from. 149 | publicPath: ifDev( 150 | // As we run a seperate server for our client and server bundles we 151 | // need to use an absolute http path for our assets public path. 152 | `http://localhost:${envVars.CLIENT_DEVSERVER_PORT}${envVars.CLIENT_BUNDLE_HTTP_PATH}`, 153 | // Otherwise we expect our bundled output to be served from this path. 154 | envVars.CLIENT_BUNDLE_HTTP_PATH 155 | ), 156 | // When in server mode we will output our bundle as a commonjs2 module. 157 | libraryTarget: ifNodeTarget('commonjs2', 'var'), 158 | }, 159 | resolve: { 160 | // These extensions are tried when resolving a file. 161 | extensions: [ 162 | '.js', 163 | '.jsx', 164 | '.json', 165 | ], 166 | // We alias out our react dependencies and replace them with the 167 | // lightweight preact library. 168 | // @see https://github.com/developit/preact-compat 169 | alias: { 170 | react: 'preact-compat', 171 | 'react-dom': 'preact-compat', 172 | 'react-dom/server': 'preact-compat', 173 | }, 174 | }, 175 | plugins: removeEmpty([ 176 | // We use the System.import feature of webpack with a "dynamic" component 177 | // path. (e.g. System.import(`../components/App/views/${viewName}/index.js`)) 178 | // This causes webpack to create a regex that will match for the dynamic 179 | // part of the path (i.e. ${viewName}). By default a greedy ".*" regex 180 | // pattern will be used, and therefore any subfolders containing a 181 | // "index.js" file will be considered a match and webpack will then 182 | // create a seperate bundle for the path. This is probably not going 183 | // to be desirable for us, so I have overridden the regex that will be 184 | // used below and have specified it in such a manner that only the root 185 | // folders within "~/src/shared/components/App/views" that contain an 186 | // "index.js" will be considered an async view component that should be 187 | // used by webpack for code splitting. 188 | // @see https://github.com/webpack/webpack/issues/87 189 | ifReactTarget( 190 | new webpack.ContextReplacementPlugin( 191 | /components[\/\\]App[\/\\]views$/, 192 | new RegExp(String.raw`^\.[\\\/](\w|\s|-|_)*[\\\/]index\.js$`) 193 | ) 194 | ), 195 | 196 | // We use this so that our generated [chunkhash]'s are only different if 197 | // the content for our respective chunks have changed. This optimises 198 | // our long term browser caching strategy for our client bundle, avoiding 199 | // cases where browsers end up having to download all the client chunks 200 | // even though 1 or 2 may have only changed. 201 | ifClient(new WebpackMd5Hash()), 202 | 203 | // The DefinePlugin is used by webpack to substitute any patterns that it 204 | // finds within the code with the respective value assigned below. 205 | // 206 | // For example you may have the following in your code: 207 | // if (process.env.NODE_ENV === 'development') { 208 | // console.log('Foo'); 209 | // } 210 | // 211 | // If we assign the NODE_ENV variable in the DefinePlugin below a value 212 | // of 'production' webpack will replace your code with the following: 213 | // if ('production' === 'development') { 214 | // console.log('Foo'); 215 | // } 216 | // 217 | // This is very useful as we are compiling/bundling our code and we would 218 | // like our environment variables to persist within the code. 219 | // 220 | // Each key passed into DefinePlugin is an identifier. 221 | // The values for each key will be inlined into the code replacing any 222 | // instances of the keys that are found. 223 | // If the value is a string it will be used as a code fragment. 224 | // If the value isn’t a string, it will be stringified (including functions). 225 | // If the value is an object all keys are removeEmpty the same way. 226 | // If you prefix typeof to the key, it’s only removeEmpty for typeof calls. 227 | new webpack.DefinePlugin( 228 | merge( 229 | { 230 | // NOTE: The NODE_ENV key is especially important for production 231 | // builds as React relies on process.env.NODE_ENV for optimizations. 232 | 'process.env.NODE_ENV': JSON.stringify(mode), 233 | // NOTE: If you are providing any environment variables from the 234 | // command line rather than the .env files then you must make sure 235 | // you add them here so that webpack can use them in during the 236 | // compiling process. 237 | // e.g. 238 | // 'process.env.MY_CUSTOM_VAR': JSON.stringify(process.env.MY_CUSTOM_VAR) 239 | }, 240 | // Now we will expose all of the .env config variables to webpack 241 | // so that it can make all the subtitutions for us. 242 | // Note: ALL of these values will be given as string types. Even if you 243 | // set numeric/boolean looking values within your .env file. The parsing 244 | // that we do of the .env file always returns the values as strings. 245 | // Therefore in your code you may need to do operations like the 246 | // following: 247 | // const MY_NUMBER = parseInt(process.env.MY_NUMBER, 10); 248 | // const MY_BOOL = process.env.MY_BOOL === 'true'; 249 | Object.keys(envVars).reduce((acc, cur) => { 250 | acc[`process.env.${cur}`] = JSON.stringify(envVars[cur]); // eslint-disable-line no-param-reassign 251 | return acc; 252 | }, {}) 253 | ) 254 | ), 255 | 256 | ifClient( 257 | // Generates a JSON file containing a map of all the output files for 258 | // our webpack bundle. A necessisty for our server rendering process 259 | // as we need to interogate these files in order to know what JS/CSS 260 | // we need to inject into our HTML. 261 | new AssetsPlugin({ 262 | filename: envVars.BUNDLE_ASSETS_FILENAME, 263 | path: path.resolve(appRootPath, envVars.BUNDLE_OUTPUT_PATH, `./${target}`), 264 | }) 265 | ), 266 | 267 | // We don't want webpack errors to occur during development as it will 268 | // kill our dev servers. 269 | ifDev(new webpack.NoErrorsPlugin()), 270 | 271 | // We need this plugin to enable hot module reloading for our dev server. 272 | ifDevClient(new webpack.HotModuleReplacementPlugin()), 273 | 274 | // Adds options to all of our loaders. 275 | ifProdClient( 276 | new webpack.LoaderOptionsPlugin({ 277 | // Indicates to our loaders that they should minify their output 278 | // if they have the capability to do so. 279 | minimize: true, 280 | // Indicates to our loaders that they should enter into debug mode 281 | // should they support it. 282 | debug: false, 283 | }) 284 | ), 285 | 286 | ifProdClient( 287 | // JS Minification. 288 | new webpack.optimize.UglifyJsPlugin({ 289 | // sourceMap: true, 290 | compress: { 291 | screw_ie8: true, 292 | warnings: false, 293 | }, 294 | mangle: { 295 | screw_ie8: true, 296 | }, 297 | output: { 298 | comments: false, 299 | screw_ie8: true, 300 | }, 301 | }) 302 | ), 303 | 304 | ifProdClient( 305 | // This is actually only useful when our deps are installed via npm2. 306 | // In npm2 its possible to get duplicates of dependencies bundled 307 | // given the nested module structure. npm3 is flat, so this doesn't 308 | // occur. 309 | new webpack.optimize.DedupePlugin() 310 | ), 311 | 312 | ifProdClient( 313 | // This is a production client so we will extract our CSS into 314 | // CSS files. 315 | new ExtractTextPlugin({ filename: '[name]-[chunkhash].css', allChunks: true }) 316 | ), 317 | ]), 318 | module: { 319 | rules: [ 320 | // Javascript 321 | { 322 | test: /\.jsx?$/, 323 | loader: 'babel-loader', 324 | include: [ 325 | path.resolve(appRootPath, './src'), 326 | // preact-compat has some es6 requirements. :) 327 | path.resolve(appRootPath, './node_modules/preact-compat'), 328 | ], 329 | query: { 330 | presets: [ 331 | // JSX 332 | 'react', 333 | // All the latest JS goodies, except for ES6 modules which 334 | // webpack has native support for and uses in the tree shaking 335 | // process. 336 | // TODO: When babel-preset-latest-minimal has stabilised use it 337 | // for our node targets so that only the missing features for 338 | // our respective node version will be transpiled. 339 | ['latest', { modules: false }], 340 | ], 341 | }, 342 | }, 343 | 344 | // JSON 345 | { 346 | test: /\.json$/, 347 | loader: 'json-loader', 348 | }, 349 | 350 | // Images and Fonts 351 | { 352 | test: /\.(jpg|jpeg|png|gif|ico|eot|svg|ttf|woff|woff2|otf)$/, 353 | loader: 'url-loader', 354 | query: { 355 | // Any file with a byte smaller than this will be "inlined" via 356 | // a base64 representation. 357 | limit: 10000, 358 | // We only emit files when building a client bundle, for the server 359 | // bundles this will just make sure any file imports will not fall 360 | // over. 361 | emitFile: isClient, 362 | }, 363 | }, 364 | 365 | // CSS 366 | merge( 367 | { test: /\.css$/ }, 368 | // When targetting the server we use the "/locals" version of the 369 | // css loader. 370 | ifNodeTarget({ 371 | loaders: [ 372 | 'css-loader/locals', 373 | ], 374 | }), 375 | // For a production client build we use the ExtractTextPlugin which 376 | // will extract our CSS into CSS files. The plugin needs to be 377 | // registered within the plugins section too. 378 | ifProdClient({ 379 | loader: ExtractTextPlugin.extract({ 380 | fallbackLoader: 'style-loader', 381 | loader: 'css-loader', 382 | }), 383 | }), 384 | // For a development client we will use a straight style & css loader 385 | // along with source maps. This combo gives us a better development 386 | // experience. 387 | ifDevClient({ 388 | loaders: [ 389 | 'style-loader', 390 | { loader: 'css-loader', query: { sourceMap: true } }, 391 | ], 392 | }) 393 | ), 394 | ], 395 | }, 396 | }; 397 | } 398 | 399 | module.exports = webpackConfigFactory; 400 | -------------------------------------------------------------------------------- /tools/webpack/server.config.js: -------------------------------------------------------------------------------- 1 | const webpackConfigFactory = require('./configFactory'); 2 | 3 | module.exports = function serverConfigFactory(options = {}, args = {}) { 4 | const { mode = 'production' } = options; 5 | return webpackConfigFactory({ target: 'server', mode }, args); 6 | }; 7 | -------------------------------------------------------------------------------- /tools/webpack/universalMiddleware.config.js: -------------------------------------------------------------------------------- 1 | const webpackConfigFactory = require('./configFactory'); 2 | 3 | module.exports = function serverConfigFactory(options = {}, args = {}) { 4 | const { mode = 'production' } = options; 5 | return webpackConfigFactory({ target: 'universalMiddleware', mode }, args); 6 | }; 7 | --------------------------------------------------------------------------------