diff --git a/config/webpack.config.dev.js b/config/webpack.config.dev.js
index 2a163eb4fa0..9cf72bff3f1 100644
--- a/config/webpack.config.dev.js
+++ b/config/webpack.config.dev.js
@@ -10,7 +10,6 @@
var path = require('path');
var autoprefixer = require('autoprefixer');
var webpack = require('webpack');
-var HtmlWebpackPlugin = require('html-webpack-plugin');
// TODO: hide this behind a flag and eliminate dead code on eject.
// This shouldn't be exposed to the user.
@@ -25,8 +24,6 @@ if (isInDebugMode) {
}
var srcPath = path.resolve(__dirname, relativePath, 'src');
var nodeModulesPath = path.join(__dirname, '..', 'node_modules');
-var indexHtmlPath = path.resolve(__dirname, relativePath, 'index.html');
-var faviconPath = path.resolve(__dirname, relativePath, 'favicon.ico');
var buildPath = path.join(__dirname, isInNodeModules ? '../../..' : '..', 'build');
module.exports = {
@@ -34,7 +31,7 @@ module.exports = {
entry: [
require.resolve('webpack-dev-server/client') + '?http://localhost:3000',
require.resolve('webpack/hot/dev-server'),
- path.join(srcPath, 'index')
+ path.join(srcPath, 'client/index')
],
output: {
// Next line is not used in dev but WebpackDevServer crashes without it:
@@ -92,11 +89,6 @@ module.exports = {
return [autoprefixer];
},
plugins: [
- new HtmlWebpackPlugin({
- inject: true,
- template: indexHtmlPath,
- favicon: faviconPath,
- }),
new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"development"' }),
// Note: only CSS is currently hot reloaded
new webpack.HotModuleReplacementPlugin()
diff --git a/config/webpack.config.prod.js b/config/webpack.config.prod.js
index a5c52513f2b..d15e7edf6e9 100644
--- a/config/webpack.config.prod.js
+++ b/config/webpack.config.prod.js
@@ -10,7 +10,6 @@
var path = require('path');
var autoprefixer = require('autoprefixer');
var webpack = require('webpack');
-var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var url = require('url');
@@ -24,9 +23,7 @@ if (process.argv[2] === '--debug-template') {
}
var srcPath = path.resolve(__dirname, relativePath, 'src');
var nodeModulesPath = path.join(__dirname, '..', 'node_modules');
-var indexHtmlPath = path.resolve(__dirname, relativePath, 'index.html');
-var faviconPath = path.resolve(__dirname, relativePath, 'favicon.ico');
-var buildPath = path.join(__dirname, isInNodeModules ? '../../..' : '..', 'build');
+var buildPath = path.join(__dirname, isInNodeModules ? '../../..' : '..', 'build/client');
var homepagePath = require(path.resolve(__dirname, relativePath, 'package.json')).homepage;
var publicPath = homepagePath ? url.parse(homepagePath).pathname : '/';
if (!publicPath.endsWith('/')) {
@@ -37,7 +34,7 @@ if (!publicPath.endsWith('/')) {
module.exports = {
bail: true,
devtool: 'source-map',
- entry: path.join(srcPath, 'index'),
+ entry: path.join(srcPath, 'client/index'),
output: {
path: buildPath,
filename: '[name].[chunkhash].js',
@@ -98,23 +95,6 @@ module.exports = {
return [autoprefixer];
},
plugins: [
- new HtmlWebpackPlugin({
- inject: true,
- template: indexHtmlPath,
- favicon: faviconPath,
- minify: {
- removeComments: true,
- collapseWhitespace: true,
- removeRedundantAttributes: true,
- useShortDoctype: true,
- removeEmptyAttributes: true,
- removeStyleLinkTypeAttributes: true,
- keepClosingSlash: true,
- minifyJS: true,
- minifyCSS: true,
- minifyURLs: true
- }
- }),
new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"production"' }),
new webpack.optimize.OccurrenceOrderPlugin(),
new webpack.optimize.DedupePlugin(),
@@ -131,6 +111,13 @@ module.exports = {
screw_ie8: true
}
}),
- new ExtractTextPlugin('[name].[contenthash].css')
+ new ExtractTextPlugin('[name].[contenthash].css'),
+ function() {
+ this.plugin('done', function(stats) {
+ require('fs').writeFileSync(
+ path.join(buildPath, 'stats.json'),
+ JSON.stringify(stats.toJson().assetsByChunkName));
+ });
+ }
]
};
diff --git a/config/webpack.config.server.dev.js b/config/webpack.config.server.dev.js
new file mode 100644
index 00000000000..dad23c8b955
--- /dev/null
+++ b/config/webpack.config.server.dev.js
@@ -0,0 +1,68 @@
+var path = require('path');
+var fs = require('fs');
+var webpack = require('webpack');
+
+var isInNodeModules = 'node_modules' ===
+ path.basename(path.resolve(path.join(__dirname, '..', '..')));
+var relativePath = isInNodeModules ? '../../..' : '..';
+var isInDebugMode = process.argv.some(arg =>
+ arg.indexOf('--debug-template') > -1
+);
+if (isInDebugMode) {
+ relativePath = '../template';
+}
+
+var srcPath = path.resolve(__dirname, relativePath, 'src');
+var nodeModulesPath = path.join(__dirname, '..', 'node_modules');
+var buildPath = path.join(__dirname, isInNodeModules ? '../../..' : '..', 'build/server');
+
+const nodeModules = fs.readdirSync(nodeModulesPath)
+ .filter(entry => ['.bin'].indexOf(entry) === -1)
+ .reduce((reduction, entry, foo) => {
+ const objectWithCommonJsModule = {};
+ objectWithCommonJsModule[entry] = `commonjs ${entry}`;
+ return Object.assign(reduction, objectWithCommonJsModule);
+ }, {});
+
+module.exports = {
+ entry: path.join(srcPath, 'server/server'),
+ target: 'node',
+ debug: true,
+ watch: true,
+ inline: true,
+ devtool: 'eval',
+ output: {
+ path: buildPath,
+ filename: 'server-dev.js',
+ publicPath: '/'
+ },
+ externals: nodeModules,
+ module: {
+ loaders: [
+ {
+ test: /\.js$/,
+ include: srcPath,
+ loader: 'babel',
+ query: require('./babel.dev')
+ },
+ {
+ test: /\.json$/,
+ loader: 'json'
+ },
+ {
+ test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)$/,
+ loader: 'file?emitFile=false',
+ },
+ {
+ test: /\.(mp4|webm)$/,
+ loader: 'url?limit=10000'
+ }
+ ]
+ },
+ resolve: {
+ extensions: ['', '.js']
+ },
+ plugins: [
+ new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"development"' }),
+ ]
+};
diff --git a/config/webpack.config.server.prod.js b/config/webpack.config.server.prod.js
new file mode 100644
index 00000000000..bede5a07b30
--- /dev/null
+++ b/config/webpack.config.server.prod.js
@@ -0,0 +1,80 @@
+var path = require('path');
+var fs = require('fs');
+var webpack = require('webpack');
+
+var isInNodeModules = 'node_modules' ===
+ path.basename(path.resolve(path.join(__dirname, '..', '..')));
+var relativePath = isInNodeModules ? '../../..' : '..';
+var isInDebugMode = process.argv.some(arg =>
+ arg.indexOf('--debug-template') > -1
+);
+if (isInDebugMode) {
+ relativePath = '../template';
+}
+
+var srcPath = path.resolve(__dirname, relativePath, 'src');
+var nodeModulesPath = path.join(__dirname, '..', 'node_modules');
+var buildPath = path.join(__dirname, isInNodeModules ? '../../..' : '..', 'build/server');
+
+const nodeModules = fs.readdirSync(nodeModulesPath)
+ .filter(entry => ['.bin'].indexOf(entry) === -1)
+ .reduce((reduction, entry, foo) => {
+ const objectWithCommonJsModule = {};
+ objectWithCommonJsModule[entry] = `commonjs ${entry}`;
+ return Object.assign(reduction, objectWithCommonJsModule);
+ }, {});
+
+module.exports = {
+ entry: path.join(srcPath, 'server/server'),
+ target: 'node',
+ devtool: 'source-map',
+ output: {
+ path: buildPath,
+ filename: 'server.js',
+ publicPath: '/'
+ },
+ externals: nodeModules,
+ module: {
+ loaders: [
+ {
+ test: /\.js$/,
+ include: srcPath,
+ loader: 'babel',
+ query: require('./babel.prod')
+ },
+ {
+ test: /\.json$/,
+ loader: 'json'
+ },
+ {
+ test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)$/,
+ loader: 'file?emitFile=false',
+ },
+ {
+ test: /\.(mp4|webm)$/,
+ loader: 'url?limit=10000'
+ }
+ ]
+ },
+ resolve: {
+ extensions: ['', '.js']
+ },
+ plugins: [
+ new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"production"' }),
+ new webpack.optimize.OccurrenceOrderPlugin(),
+ new webpack.optimize.DedupePlugin(),
+ new webpack.optimize.UglifyJsPlugin({
+ compressor: {
+ screw_ie8: true,
+ warnings: false
+ },
+ mangle: {
+ screw_ie8: true
+ },
+ output: {
+ comments: false,
+ screw_ie8: true
+ }
+ })
+ ]
+};
diff --git a/foobar/package.json b/foobar/package.json
new file mode 100644
index 00000000000..6f84cbe0cc4
--- /dev/null
+++ b/foobar/package.json
@@ -0,0 +1 @@
+{"name":"foobar","version":"0.0.1","private":true}
\ No newline at end of file
diff --git a/package.json b/package.json
index 0bf487482aa..d7efc08c545 100644
--- a/package.json
+++ b/package.json
@@ -48,11 +48,12 @@
"extract-text-webpack-plugin": "1.0.1",
"file-loader": "0.9.0",
"fs-extra": "^0.30.0",
- "html-webpack-plugin": "2.22.0",
"json-loader": "0.5.4",
"opn": "4.0.2",
"postcss-loader": "0.9.1",
+ "request": "^2.74.0",
"rimraf": "2.5.3",
+ "single-child": "^0.3.4",
"style-loader": "0.13.1",
"url-loader": "0.5.7",
"webpack": "1.13.1",
diff --git a/scripts/build.js b/scripts/build.js
index 7acb199652d..9d7e59d5514 100644
--- a/scripts/build.js
+++ b/scripts/build.js
@@ -12,7 +12,8 @@ process.env.NODE_ENV = 'production';
var path = require('path');
var rimrafSync = require('rimraf').sync;
var webpack = require('webpack');
-var config = require('../config/webpack.config.prod');
+var configClient = require('../config/webpack.config.prod');
+var configServer = require('../config/webpack.config.server.prod');
var isInNodeModules = 'node_modules' ===
path.basename(path.resolve(path.join(__dirname, '..', '..')));
@@ -24,7 +25,9 @@ var packageJsonPath = path.join(__dirname, relative, 'package.json');
var buildPath = path.join(__dirname, relative, 'build');
rimrafSync(buildPath);
-webpack(config).run(function(err, stats) {
+webpack(configServer).run(() => {});
+
+webpack(configClient).run(function(err, stats) {
if (err) {
console.error('Failed to create a production build. Reason:');
console.error(err.message || err);
diff --git a/scripts/start.js b/scripts/start.js
index 8753c34d68d..93dcb50bcce 100644
--- a/scripts/start.js
+++ b/scripts/start.js
@@ -12,19 +12,30 @@ process.env.NODE_ENV = 'development';
var path = require('path');
var chalk = require('chalk');
var webpack = require('webpack');
+var request = require('request');
+var UrlResolver = require('url');
+var SingleChild = require('single-child');
var WebpackDevServer = require('webpack-dev-server');
-var config = require('../config/webpack.config.dev');
+var configClient = require('../config/webpack.config.dev');
+var configServer = require('../config/webpack.config.server.dev');
var execSync = require('child_process').execSync;
var opn = require('opn');
+process.on('uncaughtException', err => console.error(err));
+
+const SERVER_PATH = 'http://localhost:3001';
+const MAX_PROXY_RETRIES = 3;
+
+let server = null;
+
// TODO: hide this behind a flag and eliminate dead code on eject.
// This shouldn't be exposed to the user.
-var handleCompile;
+var handleCompileClient;
var isSmokeTest = process.argv.some(arg =>
arg.indexOf('--smoke-test') > -1
);
if (isSmokeTest) {
- handleCompile = function (err, stats) {
+ handleCompileClient = function (err, stats) {
if (err || stats.hasErrors() || stats.hasWarnings()) {
process.exit(1);
} else {
@@ -61,16 +72,37 @@ function formatMessage(message) {
.replace('./~/css-loader!./~/postcss-loader!', '');
}
+function openBrowser() {
+ if (process.platform === 'darwin') {
+ try {
+ // Try our best to reuse existing tab
+ // on OS X Google Chrome with AppleScript
+ execSync('ps cax | grep "Google Chrome"');
+ execSync(
+ 'osascript ' +
+ path.resolve(__dirname, './openChrome.applescript') +
+ ' http://localhost:3000/'
+ );
+ return;
+ } catch (err) {
+ // Ignore errors.
+ }
+ }
+ // Fallback to opn
+ // (It will always open new tab)
+ opn('http://localhost:3000/');
+}
+
function clearConsole() {
process.stdout.write('\x1B[2J\x1B[0f');
}
-var compiler = webpack(config, handleCompile);
-compiler.plugin('invalid', function () {
+function webpackOnInvalid() {
clearConsole();
- console.log('Compiling...');
-});
-compiler.plugin('done', function (stats) {
+ console.log('Compiling...');
+}
+
+function webpackOnDone(stats) {
clearConsole();
var hasErrors = stats.hasErrors();
var hasWarnings = stats.hasWarnings();
@@ -119,35 +151,63 @@ compiler.plugin('done', function (stats) {
console.log('Use ' + chalk.yellow('// eslint-disable-next-line') + ' to ignore the next line.');
console.log('Use ' + chalk.yellow('/* eslint-disable */') + ' to ignore all warnings in a file.');
}
-});
+}
-function openBrowser() {
- if (process.platform === 'darwin') {
- try {
- // Try our best to reuse existing tab
- // on OS X Google Chrome with AppleScript
- execSync('ps cax | grep "Google Chrome"');
- execSync(
- 'osascript ' +
- path.resolve(__dirname, './openChrome.applescript') +
- ' http://localhost:3000/'
- );
- return;
- } catch (err) {
- // Ignore errors.
- }
- }
- // Fallback to opn
- // (It will always open new tab)
- opn('http://localhost:3000/');
+function exponentialBackoff(step) {
+ return Math.pow(2, step);
}
-new WebpackDevServer(compiler, {
+const clientCompiler = webpack(configClient, handleCompileClient);
+clientCompiler.plugin('invalid', webpackOnInvalid);
+clientCompiler.plugin('done', webpackOnDone);
+
+// Here's the server compiler
+webpack(configServer, function(error, stats) {
+ webpackOnDone(stats);
+
+ if (!server) {
+ server = new SingleChild('node', ['build/server/server-dev.js'], {
+ stdio: [0, 1, 2]
+ });
+ server.start();
+ } else {
+ server.restart();
+ }
+});
+
+const devServer = new WebpackDevServer(clientCompiler, {
historyApiFallback: true,
hot: true, // Note: only CSS is currently hot reloaded
- publicPath: config.output.publicPath,
+ publicPath: configClient.output.publicPath,
quiet: true
-}).listen(3000, function (err, result) {
+});
+
+devServer.use('/', (req, res) => {
+ const url = UrlResolver.resolve(SERVER_PATH, req.url);
+
+ let retries = 0;
+ const proxyRequest = () => {
+ req
+ .pipe(request(url))
+ .on('error', error => {
+ if (retries <= MAX_PROXY_RETRIES) {
+ setTimeout(proxyRequest, exponentialBackoff(retries) * 1000)
+ retries++;
+ } else {
+ console.error(error);
+
+ res
+ .status(500)
+ .send('Proxy does not work');
+ }
+ })
+ .pipe(res);
+ }
+
+ proxyRequest();
+});
+
+devServer.listen(3000, function (err, result) {
if (err) {
return console.log(err);
}
@@ -157,3 +217,5 @@ new WebpackDevServer(compiler, {
console.log();
openBrowser();
});
+
+
diff --git a/template/package.json b/template/package.json
index e74146c7029..02c2ec61e2f 100644
--- a/template/package.json
+++ b/template/package.json
@@ -7,10 +7,12 @@
},
"dependencies": {
"react": "^15.2.1",
- "react-dom": "^15.2.1"
+ "react-dom": "^15.2.1",
+ "express": "^4.13.4"
},
"scripts": {
"start": "react-scripts start",
+ "start:production": "node build/server/server.js",
"build": "react-scripts build",
"eject": "react-scripts eject"
}
diff --git a/template/src/App.css b/template/src/client/App.css
similarity index 100%
rename from template/src/App.css
rename to template/src/client/App.css
diff --git a/template/src/App.js b/template/src/client/App.js
similarity index 95%
rename from template/src/App.js
rename to template/src/client/App.js
index d7d52a7f38a..75aea8d9398 100644
--- a/template/src/App.js
+++ b/template/src/client/App.js
@@ -1,6 +1,5 @@
import React, { Component } from 'react';
import logo from './logo.svg';
-import './App.css';
class App extends Component {
render() {
diff --git a/template/src/index.css b/template/src/client/index.css
similarity index 100%
rename from template/src/index.css
rename to template/src/client/index.css
diff --git a/template/src/index.js b/template/src/client/index.js
similarity index 89%
rename from template/src/index.js
rename to template/src/client/index.js
index 54c5ef1a427..02fb078e791 100644
--- a/template/src/index.js
+++ b/template/src/client/index.js
@@ -1,7 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
+
import './index.css';
+import './App.css';
ReactDOM.render(