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( , diff --git a/template/src/logo.svg b/template/src/client/logo.svg similarity index 100% rename from template/src/logo.svg rename to template/src/client/logo.svg diff --git a/template/src/server/server.js b/template/src/server/server.js new file mode 100644 index 00000000000..10ee4849295 --- /dev/null +++ b/template/src/server/server.js @@ -0,0 +1,23 @@ +import express from 'express'; +import React from 'react'; +import { renderToString } from 'react-dom/server'; +import fs from 'fs'; + +import App from '../client/App.js'; +import template from './template.js'; + +const isDevelopment = () => process.env.NODE_ENV === 'development'; + +const config = !isDevelopment() ? JSON.parse(fs.readFileSync('./build/client/stats.json', { encoding: 'utf8' })) : null; + +const jsPath = config ? config.main.find(fileName => fileName.endsWith('.js')) : '/bundle.js'; +const cssPath = config ? config.main.find(fileName => fileName.endsWith('.css')) : null; + +const server = express(); +server.use('/', express.static('build/client')); +server.get('*', (req, res) => res + .send(template(renderToString(), jsPath, cssPath))); + +const port = process.env.NODE_ENV === 'development' ? 3001 : 3000; +server.listen(port); + diff --git a/template/index.html b/template/src/server/template.js similarity index 57% rename from template/index.html rename to template/src/server/template.js index 72e10e94c6c..8f36fcc5b0e 100644 --- a/template/index.html +++ b/template/src/server/template.js @@ -1,12 +1,14 @@ +export default (content, jsPath, cssPath) => ` React App + ${cssPath ? `` : ''} -
+
${content}
+ +`;