diff --git a/.gitignore b/.gitignore index 1900a2f2cf9..e69b43c07bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/ build +staging .DS_Store *.tgz my-app* diff --git a/package.json b/package.json index 1029bc47758..ab5e9aefdc4 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "e2e": "tasks/e2e.sh", "postinstall": "lerna bootstrap", "publish": "tasks/release.sh", + "stage": "node packages/react-scripts/scripts/stage.js", "start": "node packages/react-scripts/scripts/start.js", "test": "node packages/react-scripts/scripts/test.js --env=jsdom" }, diff --git a/packages/babel-preset-react-app/index.js b/packages/babel-preset-react-app/index.js index 34e9efabb72..26a949afdce 100644 --- a/packages/babel-preset-react-app/index.js +++ b/packages/babel-preset-react-app/index.js @@ -45,11 +45,11 @@ module.exports = { // https://github.com/facebookincubator/create-react-app/issues/720 // It’s also nice that we can enforce `NODE_ENV` being specified. var env = process.env.BABEL_ENV || process.env.NODE_ENV; -if (env !== 'development' && env !== 'test' && env !== 'production') { +if (env !== 'development' && env !== 'test' && env !== 'production' && env !== 'staging') { throw new Error( 'Using `babel-preset-react-app` requires that you specify `NODE_ENV` or '+ '`BABEL_ENV` environment variables. Valid values are "development", ' + - '"test", and "production". Instead, received: ' + JSON.stringify(env) + '.' + '"test", "staging", and "production". Instead, received: ' + JSON.stringify(env) + '.' ); } var plugins = module.exports.plugins; @@ -61,7 +61,7 @@ if (env === 'development' || env === 'test') { require.resolve('babel-plugin-transform-react-jsx-self') ]); } -if (env === 'production') { +if (env === 'production' || env === 'staging') { // Optimization: hoist JSX that never changes out of render() // Disabled because of issues: // * https://github.com/facebookincubator/create-react-app/issues/525 diff --git a/packages/react-scripts/bin/react-scripts.js b/packages/react-scripts/bin/react-scripts.js index 58381833957..91d3d57e943 100755 --- a/packages/react-scripts/bin/react-scripts.js +++ b/packages/react-scripts/bin/react-scripts.js @@ -5,6 +5,7 @@ var args = process.argv.slice(3); switch (script) { case 'build': +case 'stage': case 'eject': case 'start': case 'test': diff --git a/packages/react-scripts/config/paths.js b/packages/react-scripts/config/paths.js index 1d50c38b9f8..17ef50ba689 100644 --- a/packages/react-scripts/config/paths.js +++ b/packages/react-scripts/config/paths.js @@ -38,6 +38,7 @@ var nodePaths = (process.env.NODE_PATH || '') // config after eject: we're in ./config/ module.exports = { appBuild: resolveApp('build'), + appStaging: resolveApp('staging'), appPublic: resolveApp('public'), appHtml: resolveApp('public/index.html'), appIndexJs: resolveApp('src/index.js'), @@ -57,6 +58,7 @@ function resolveOwn(relativePath) { // config before eject: we're in ./node_modules/react-scripts/config/ module.exports = { appBuild: resolveApp('build'), + appStaging: resolveApp('staging'), appPublic: resolveApp('public'), appHtml: resolveApp('public/index.html'), appIndexJs: resolveApp('src/index.js'), @@ -73,6 +75,7 @@ module.exports = { // @remove-on-publish-begin module.exports = { appBuild: resolveOwn('../../../build'), + appStaging: resolveOwn('../../../staging'), appPublic: resolveOwn('../template/public'), appHtml: resolveOwn('../template/public/index.html'), appIndexJs: resolveOwn('../template/src/index.js'), diff --git a/packages/react-scripts/config/webpack.config.prod.js b/packages/react-scripts/config/webpack.config.prod.js index 2fb3035719e..5d0e57df2db 100644 --- a/packages/react-scripts/config/webpack.config.prod.js +++ b/packages/react-scripts/config/webpack.config.prod.js @@ -47,11 +47,6 @@ var publicUrl = ensureSlash(homepagePathname, false); // Get environment variables to inject into our app. var env = getClientEnvironment(publicUrl); -// Assert this just to be safe. -// Development builds of React are slow and not intended for production. -if (env['process.env'].NODE_ENV !== '"production"') { - throw new Error('Production builds must have NODE_ENV=production.'); -} // This is the production configuration. // It compiles slowly and is focused on producing a fast and minimal bundle. diff --git a/packages/react-scripts/config/webpack.config.staging.js b/packages/react-scripts/config/webpack.config.staging.js new file mode 100644 index 00000000000..ebea66ff252 --- /dev/null +++ b/packages/react-scripts/config/webpack.config.staging.js @@ -0,0 +1,28 @@ +// @remove-on-eject-begin +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +// @remove-on-eject-end + +var webpack = require('webpack'); +var HtmlWebpackPlugin = require('html-webpack-plugin'); +var paths = require('./paths'); +var config = require('./webpack.config.prod'); + +module.exports = Object.assign(config, { + output: Object.assign(config.output, { + path: paths.appStaging, + }), + plugins: [new HtmlWebpackPlugin({ + inject: true, + template: paths.appHtml + })].concat(config.plugins.filter(plugin => + !(plugin instanceof webpack.optimize.UglifyJsPlugin) && + !(plugin instanceof HtmlWebpackPlugin) + )) +}); diff --git a/packages/react-scripts/scripts/stage.js b/packages/react-scripts/scripts/stage.js new file mode 100644 index 00000000000..8c89135e35c --- /dev/null +++ b/packages/react-scripts/scripts/stage.js @@ -0,0 +1,95 @@ +// @remove-on-eject-begin +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +// @remove-on-eject-end + +// Do this as the first thing so that any code reading it knows the right env. +process.env.NODE_ENV = 'staging'; + +// Load environment variables from .env file. Suppress warnings using silent +// if this file is missing. dotenv will never modify any environment variables +// that have already been set. +// https://github.com/motdotla/dotenv +require('dotenv').config({silent: true}); + +var chalk = require('chalk'); +var fs = require('fs-extra'); +var rimrafSync = require('rimraf').sync; +var webpack = require('webpack'); +var config = require('../config/webpack.config.staging'); +var paths = require('../config/paths'); +var checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); +var recursive = require('recursive-readdir'); + +// Warn and crash if required files are missing +if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { + process.exit(1); +} + +recursive(paths.appStaging, (err, fileNames) => { + // Remove all content but keep the directory so that + // if you're in it, you don't end up in Trash + rimrafSync(paths.appStaging + '/*'); + + // Start the webpack build + build(); + + // Merge with the public folder + copyPublicFolder(); +}); + + +// Create the staging build and print the deployment instructions. +function build() { + console.log('Creating a staging build...'); + webpack(config).run((err, stats) => { + if (err) { + console.error('Failed to create a staging build. Reason:'); + console.error(err.message || err); + process.exit(1); + } + + console.log(chalk.green('Compiled successfully.')); + console.log(); + + var openCommand = process.platform === 'win32' ? 'start' : 'open'; + var homepagePath = require(paths.appPackageJson).homepage; + var publicPath = config.output.publicPath; + if (publicPath !== '/') { + // "homepage": "http://mywebsite.com/project" + console.log('The project was built assuming it is hosted at ' + chalk.green(publicPath) + '.'); + console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.'); + console.log(); + console.log('The ' + chalk.cyan('staging') + ' folder is ready to be deployed.'); + console.log(); + } else { + // no homepage or "homepage": "http://mywebsite.com" + console.log('The project was built assuming it is hosted at the server root.'); + if (homepagePath) { + // "homepage": "http://mywebsite.com" + console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.'); + console.log(); + } + console.log('The ' + chalk.cyan('staging') + ' folder is ready to be deployed.'); + console.log('You may also serve it locally with a static server:') + console.log(); + console.log(' ' + chalk.cyan('npm') + ' install -g pushstate-server'); + console.log(' ' + chalk.cyan('pushstate-server') + ' staging'); + console.log(' ' + chalk.cyan(openCommand) + ' http://localhost:9000'); + console.log(); + } + }); +} + +function copyPublicFolder() { + fs.copySync(paths.appPublic, paths.appStaging, { + dereference: true, + filter: file => file !== paths.appHtml + }); +}