commit 8da2f4eee616cc295c07ab192b1de9e054f00110
parent 889a24bba2ae5d810dc21d79be666d51f1aac961
Author: Konstantin Tarkus <hello@tarkus.me>
Date: Fri, 18 Dec 2015 09:49:51 +0300
Improve start.js script that starts dev server with Browsersync and HMR
Diffstat:
12 files changed, 203 insertions(+), 171 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
@@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.
### [Unreleased][unreleased]
+- Optimize the `tools/start.js` script that launches dev server with Browsersync and HMR
- Replace Superagent with WHATWG Fetch library
- Rename `app.js` to `client.js` (aka client-side code)
- Integrate [CSS Modules](https://github.com/css-modules/css-modules) and
diff --git a/README.md b/README.md
@@ -18,7 +18,7 @@ See [demo](http://demo.reactstarterkit.com) |
join [#react-starter-kit](https://gitter.im/kriasoft/react-starter-kit) chatroom to stay up to date
[](https://rollbar.com/?utm_source=reactstartkit(github)&utm_medium=link&utm_campaign=reactstartkit(github))
-[](https://localizejs.com/)
+[](https://localizejs.com/?cid=802&utm_source=rsk)
### Directory Layout
@@ -46,12 +46,12 @@ join [#react-starter-kit](https://gitter.im/kriasoft/react-starter-kit) chatroom
│ ├── /build.js # Builds the project from source to output (build) folder
│ ├── /bundle.js # Bundles the web resources into package(s) through Webpack
│ ├── /clean.js # Cleans up the output (build) folder
-│ ├── /webpack.config.js # Configurations for client-side and server-side bundles
│ ├── /copy.js # Copies static files to output (build) folder
│ ├── /deploy.js # Deploys your web application
│ ├── /run.js # Helper function for running build automation tasks
-│ ├── /serve.js # Launches the Node.js/Express web server
-│ └── /start.js # Launches the development web server with "live reload"
+│ ├── /runServer.js # Launches (or restarts) Node.js server
+│ ├── /start.js # Launches the development web server with "live reload"
+│ └── /webpack.config.js # Configurations for client-side and server-side bundles
│── package.json # The list of 3rd party libraries and utilities
└── preprocessor.js # ES6 transpiler settings for Jest
```
diff --git a/package.json b/package.json
@@ -59,8 +59,8 @@
"replace": "^0.3.0",
"url-loader": "^0.5.7",
"webpack": "^1.12.9",
- "webpack-dev-middleware": "^1.4.0",
- "webpack-hot-middleware": "^2.6.0"
+ "webpack-hot-middleware": "^2.6.0",
+ "webpack-middleware": "^1.4.0"
},
"jest": {
"rootDir": "./src",
@@ -80,7 +80,6 @@
"bundle": "babel-node tools/run bundle",
"build": "babel-node tools/run build",
"deploy": "babel-node tools/run deploy",
- "serve": "babel-node tools/run serve",
"start": "babel-node tools/run start"
}
}
diff --git a/tools/README.md b/tools/README.md
@@ -0,0 +1,47 @@
+## Build Automation Tools
+
+##### `npm start` (`start.js`)
+
+* Cleans up the output `/build` directory (`clean.js`)
+* Copies static files to the output folder (`copy.js`)
+* Launches [Webpack](https://webpack.github.io/) compiler in a watch mode (via [webpack-middleware](https://github.com/kriasoft/webpack-middleware))
+* Launches Node.js server from the compiled output folder (`runServer.js`)
+* Launches [Browsersync](https://browsersync.io/),
+ [HMR](https://webpack.github.io/docs/hot-module-replacement), and
+ [React Transform](https://github.com/gaearon/babel-plugin-react-transform)
+
+##### `npm run build` (`build.js`)
+
+* Cleans up the output `/build` folder (`clean.js`)
+* Copies static files to the output folder (`copy.js`)
+* Creates application bundles with Webpack (`bundle.js`, `webpack.config.js`)
+
+##### `npm run deploy` (`deploy.js`)
+
+* Builds the project from source files (`build.js`)
+* Pushes the contents of the `/build` folder to a remote server with Git
+
+##### Options
+
+Flag | Description
+----------- | --------------------------------------------------
+`--release` | Minimizes and optimizes the compiled output
+`--verbose` | Prints detailed information to the console
+
+For example:
+
+```sh
+$ npm run build -- --release --verbose # Build the app in production mode
+```
+
+or
+
+```sh
+$ npm start -- --release # Launch dev server in production mode
+```
+
+#### Misc
+
+* `webpack.config.js` - Webpack configuration for both client-side and server-side bundles
+* `run.js` - Helps to launch other scripts with `babel-node` (e.g. `babel-node tools/run build`)
+* `.eslintrc` - ESLint overrides for built automation scripts
diff --git a/tools/build.js b/tools/build.js
@@ -8,15 +8,18 @@
*/
import run from './run';
+import clean from './clean';
+import copy from './copy';
+import bundle from './bundle';
/**
* Compiles the project from source files into a distributable
* format and copies it to the output (build) folder.
*/
async function build() {
- await run(require('./clean'));
- await run(require('./copy'));
- await run(require('./bundle'));
+ await run(clean);
+ await run(copy);
+ await run(bundle);
}
export default build;
diff --git a/tools/bundle.js b/tools/bundle.js
@@ -11,31 +11,17 @@ import webpack from 'webpack';
import webpackConfig from './webpack.config';
/**
- * Bundles JavaScript, CSS and images into one or more packages
- * ready to be used in a browser.
+ * Creates application bundles from the source files.
*/
function bundle() {
return new Promise((resolve, reject) => {
- const bundler = webpack(webpackConfig);
- let bundlerRunCount = 0;
-
- function onComplete(err, stats) {
+ webpack(webpackConfig).run((err, stats) => {
if (err) {
return reject(err);
}
-
console.log(stats.toString(webpackConfig[0].stats));
-
- if (++bundlerRunCount === (global.WATCH ? webpackConfig.length : 1)) {
- return resolve();
- }
- }
-
- if (global.WATCH) {
- bundler.watch(200, onComplete);
- } else {
- bundler.run(onComplete);
- }
+ resolve();
+ });
});
}
diff --git a/tools/copy.js b/tools/copy.js
@@ -8,15 +8,15 @@
*/
import path from 'path';
+import gaze from 'gaze';
import replace from 'replace';
import Promise from 'bluebird';
-import watch from './lib/watch';
/**
* Copies static files such as robots.txt, favicon.ico to the
* output (build) folder.
*/
-async function copy() {
+async function copy({ watch } = {}) {
const ncp = Promise.promisify(require('ncp'));
await Promise.all([
@@ -33,8 +33,10 @@ async function copy() {
silent: false,
});
- if (global.WATCH) {
- const watcher = await watch('src/content/**/*.*');
+ if (watch) {
+ const watcher = await new Promise((resolve, reject) => {
+ gaze('src/content/**/*.*', (err, val) => err ? reject(err) : resolve(val));
+ });
watcher.on('changed', async (file) => {
const relPath = file.substr(path.join(__dirname, '../src/content/').length);
await ncp(`src/content/${relPath}`, `build/content/${relPath}`);
diff --git a/tools/lib/watch.js b/tools/lib/watch.js
@@ -1,14 +0,0 @@
-/**
- * React Starter Kit (http://www.reactstarterkit.com/)
- *
- * Copyright © 2014-2015 Kriasoft, LLC. All rights reserved.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE.txt file in the root directory of this source tree.
- */
-
-import gaze from 'gaze';
-
-export default (pattern) => new Promise((resolve, reject) => {
- gaze(pattern, (err, watcher) => err ? reject(err) : resolve(watcher));
-});
diff --git a/tools/runServer.js b/tools/runServer.js
@@ -0,0 +1,58 @@
+/**
+ * React Starter Kit (http://www.reactstarterkit.com/)
+ *
+ * Copyright © 2014-2015 Kriasoft, LLC. All rights reserved.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+import path from 'path';
+import cp from 'child_process';
+import webpackConfig from './webpack.config';
+
+// Should match the text string used in `src/server.js/server.listen(...)`
+const RUNNING_REGEXP = /The server is running at http:\/\/(.*?)\//;
+
+let server;
+const { output } = webpackConfig.find(x => x.target === 'node');
+const serverPath = path.join(output.path, output.filename);
+
+// Launch or restart the Node.js server
+function runServer(cb) {
+ function onStdOut(data) {
+ const time = new Date().toTimeString();
+ const match = data.toString('utf8').match(RUNNING_REGEXP);
+
+ process.stdout.write(time.replace(/.*(\d{2}:\d{2}:\d{2}).*/, '[$1] '));
+ process.stdout.write(data);
+
+ if (match) {
+ server.stdout.removeListener('data', onStdOut);
+ server.stdout.on('data', x => process.stdout.write(x));
+ if (cb) {
+ cb(null, match[1]);
+ }
+ }
+ }
+
+ if (server) {
+ server.kill('SIGTERM');
+ }
+
+ server = cp.spawn('node', [serverPath], {
+ env: Object.assign({ NODE_ENV: 'development' }, process.env),
+ silent: false,
+ });
+
+ server.stdout.on('data', onStdOut);
+ server.stderr.on('data', x => process.stderr.write(x));
+}
+
+process.on('exit', () => {
+ if (server) {
+ server.kill('SIGTERM');
+ }
+});
+
+export default runServer;
diff --git a/tools/serve.js b/tools/serve.js
@@ -1,56 +0,0 @@
-/**
- * React Starter Kit (http://www.reactstarterkit.com/)
- *
- * Copyright © 2014-2015 Kriasoft, LLC. All rights reserved.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE.txt file in the root directory of this source tree.
- */
-
-import path from 'path';
-import cp from 'child_process';
-import watch from './lib/watch';
-
-/**
- * Launches Node.js/Express web server in a separate (forked) process.
- */
-function serve() {
- return new Promise((resolve, reject) => {
- function start() {
- const server = cp.spawn(
- 'node',
- [path.join(__dirname, '../build/server.js')],
- {
- env: Object.assign({ NODE_ENV: 'development' }, process.env),
- silent: false,
- }
- );
- server.stdout.on('data', data => {
- let time = new Date().toTimeString();
- time = time.replace(/.*(\d{2}:\d{2}:\d{2}).*/, '$1');
- process.stdout.write(`[${time}] `);
- process.stdout.write(data);
- if (data.toString('utf8').includes('The server is running at')) {
- resolve();
- }
- });
- server.stderr.on('data', data => process.stderr.write(data));
- server.on('error', err => reject(err));
- process.on('exit', () => server.kill('SIGTERM'));
- return server;
- }
-
- let server = start();
-
- if (global.WATCH) {
- watch('build/server.js').then(watcher => {
- watcher.on('changed', () => {
- server.kill('SIGTERM');
- server = start();
- });
- });
- }
- });
-}
-
-export default serve;
diff --git a/tools/start.js b/tools/start.js
@@ -7,55 +7,92 @@
* LICENSE.txt file in the root directory of this source tree.
*/
-import browserSync from 'browser-sync';
+import Browsersync from 'browser-sync';
import webpack from 'webpack';
-import webpackDevMiddleware from 'webpack-dev-middleware';
+import webpackMiddleware from 'webpack-middleware';
import webpackHotMiddleware from 'webpack-hot-middleware';
import run from './run';
-
-global.WATCH = true;
-const webpackConfig = require('./webpack.config')[0]; // Client-side bundle configuration
-const bundler = webpack(webpackConfig);
+import runServer from './runServer';
+import webpackConfig from './webpack.config';
+import clean from './clean';
+import copy from './copy';
/**
* Launches a development web server with "live reload" functionality -
* synchronizing URLs, interactions and code changes across multiple devices.
*/
async function start() {
- await run(require('./build'));
- await run(require('./serve'));
-
- browserSync({
- proxy: {
-
- target: 'localhost:5000',
-
- middleware: [
- webpackDevMiddleware(bundler, {
- // IMPORTANT: dev middleware can't access config, so we should
- // provide publicPath by ourselves
- publicPath: webpackConfig.output.publicPath,
-
- // Pretty colored output
- stats: webpackConfig.stats,
+ await run(clean);
+ await run(copy.bind(undefined, { watch: true }));
+ await new Promise(resolve => {
+ // Patch the client-side bundle configurations
+ // to enable Hot Module Replacement (HMR) and React Transform
+ webpackConfig.filter(x => x.target !== 'node').forEach(config => {
+ if (Array.isArray(config.entry)) {
+ config.entry.unshift('webpack-hot-middleware/client');
+ } else {
+ config.entry = ['webpack-hot-middleware/client', config.entry];
+ }
+ config.plugins.push(new webpack.HotModuleReplacementPlugin());
+ config.plugins.push(new webpack.NoErrorsPlugin());
+ config.module.loaders
+ .filter(x => x.loader === 'babel-loader')
+ .forEach(x => x.query = {
+ // Wraps all React components into arbitrary transforms
+ // https://github.com/gaearon/babel-plugin-react-transform
+ plugins: ['react-transform'],
+ extra: {
+ 'react-transform': {
+ transforms: [
+ {
+ transform: 'react-transform-hmr',
+ imports: ['react'],
+ locals: ['module'],
+ }, {
+ transform: 'react-transform-catch-errors',
+ imports: ['react', 'redbox-react'],
+ },
+ ],
+ },
+ },
+ });
+ });
- // For other settings see
- // http://webpack.github.io/docs/webpack-dev-middleware.html
- }),
+ const bundler = webpack(webpackConfig);
+ const wpMiddleware = webpackMiddleware(bundler, {
+ // IMPORTANT: webpack middleware can't access config,
+ // so we should provide publicPath by ourselves
+ publicPath: webpackConfig[0].output.publicPath,
+ // Pretty colored output
+ stats: webpackConfig[0].stats,
+ // For other settings see
+ // https://webpack.github.io/docs/webpack-dev-middleware
+ });
+ const hotMiddlewares = bundler.compilers
+ .filter(compiler => compiler.options.target !== 'node')
+ .map(compiler => webpackHotMiddleware(compiler));
- // bundler should be the same as above
- webpackHotMiddleware(bundler),
- ],
- },
+ let handleServerBundleComplete = () => {
+ runServer((err, host) => {
+ if (!err) {
+ const bs = Browsersync.create();
+ bs.init({
+ proxy: {
+ target: host,
+ middleware: [wpMiddleware, ...hotMiddlewares],
+ },
+ // no need to watch '*.js' here, webpack will take care of it for us,
+ // including full page reloads if HMR won't work
+ files: ['build/content/**/*.*'],
+ }, resolve);
+ handleServerBundleComplete = () => runServer();
+ }
+ });
+ };
- // no need to watch '*.js' here, webpack will take care of it for us,
- // including full page reloads if HMR won't work
- files: [
- 'build/public/**/*.css',
- 'build/public/**/*.html',
- 'build/content/**/*.*',
- 'build/templates/**/*.*',
- ],
+ bundler.compilers
+ .find(x => x.options.target === 'node')
+ .plugin('done', () => handleServerBundleComplete());
});
}
diff --git a/tools/webpack.config.js b/tools/webpack.config.js
@@ -14,7 +14,6 @@ import AssetsPlugin from 'assets-webpack-plugin';
const DEBUG = !process.argv.includes('--release');
const VERBOSE = process.argv.includes('--verbose');
-const WATCH = global.WATCH === undefined ? false : global.WATCH;
const AUTOPREFIXER_BROWSERS = [
'Android 2.3',
'Android >= 4',
@@ -111,10 +110,7 @@ const config = {
// -----------------------------------------------------------------------------
const clientConfig = merge({}, config, {
- entry: [
- ...(WATCH ? ['webpack-hot-middleware/client'] : []),
- './src/client.js',
- ],
+ entry: './src/client.js',
output: {
path: path.join(__dirname, '../build/public'),
filename: DEBUG ? '[name].js?[hash]' : '[name].[hash].js',
@@ -138,36 +134,9 @@ const clientConfig = merge({}, config, {
}),
new webpack.optimize.AggressiveMergingPlugin(),
] : []),
- ...(WATCH ? [
- new webpack.HotModuleReplacementPlugin(),
- new webpack.NoErrorsPlugin(),
- ] : []),
],
});
-// Enable React Transform in the "watch" mode
-clientConfig.module.loaders
- .filter(x => WATCH && x.loader === 'babel-loader')
- .forEach(x => x.query = {
- // Wraps all React components into arbitrary transforms
- // https://github.com/gaearon/babel-plugin-react-transform
- plugins: ['react-transform'],
- extra: {
- 'react-transform': {
- transforms: [
- {
- transform: 'react-transform-hmr',
- imports: ['react'],
- locals: ['module'],
- }, {
- transform: 'react-transform-catch-errors',
- imports: ['react', 'redbox-react'],
- },
- ],
- },
- },
- });
-
//
// Configuration for the server-side bundle (server.js)
// -----------------------------------------------------------------------------