From ce7917749a64f6d211a7d3bdb2ab814ea882a5a7 Mon Sep 17 00:00:00 2001 From: Eduard Kyvenko Date: Sun, 28 May 2017 02:30:01 +0200 Subject: [PATCH] feat(scripts): Improved the start script The start script now supports the option to use a different port and has better messages Closes #36 --- config/webpackDevServer.config.js | 90 ++++++++++ scripts/start.js | 276 +++++++++++++++--------------- 2 files changed, 230 insertions(+), 136 deletions(-) create mode 100644 config/webpackDevServer.config.js diff --git a/config/webpackDevServer.config.js b/config/webpackDevServer.config.js new file mode 100644 index 00000000..d57e77a8 --- /dev/null +++ b/config/webpackDevServer.config.js @@ -0,0 +1,90 @@ +const errorOverlayMiddleware = require('react-error-overlay/middleware') +const noopServiceWorkerMiddleware = require('react-dev-utils/noopServiceWorkerMiddleware') +const config = require('./webpack.config.dev') +const paths = require('./paths') + +const protocol = process.env.HTTPS === 'true' ? 'https' : 'http' +const host = process.env.HOST || '0.0.0.0' + +module.exports = function (proxy, allowedHost) { + return { + // WebpackDevServer 2.4.3 introduced a security fix that prevents remote + // websites from potentially accessing local content through DNS rebinding: + // https://github.com/webpack/webpack-dev-server/issues/887 + // https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a + // However, it made several existing use cases such as development in cloud + // environment or subdomains in development significantly more complicated: + // https://github.com/facebookincubator/create-react-app/issues/2271 + // https://github.com/facebookincubator/create-react-app/issues/2233 + // While we're investigating better solutions, for now we will take a + // compromise. Since our WDS configuration only serves files in the `public` + // folder we won't consider accessing them a vulnerability. However, if you + // use the `proxy` feature, it gets more dangerous because it can expose + // remote code execution vulnerabilities in backends like Django and Rails. + // So we will disable the host check normally, but enable it if you have + // specified the `proxy` setting. Finally, we let you override it if you + // really know what you're doing with a special environment variable. + disableHostCheck: !proxy || + process.env.DANGEROUSLY_DISABLE_HOST_CHECK === 'true', + // Enable gzip compression of generated files. + compress: true, + // Silence WebpackDevServer's own logs since they're generally not useful. + // It will still show compile warnings and errors with this setting. + clientLogLevel: 'none', + // By default WebpackDevServer serves physical files from current directory + // in addition to all the virtual build products that it serves from memory. + // This is confusing because those files won’t automatically be available in + // production build folder unless we copy them. However, copying the whole + // project directory is dangerous because we may expose sensitive files. + // Instead, we establish a convention that only files in `public` directory + // get served. Our build script will copy `public` into the `build` folder. + // In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%: + // + // In JavaScript code, you can access it with `process.env.PUBLIC_URL`. + // Note that we only recommend to use `public` folder as an escape hatch + // for files like `favicon.ico`, `manifest.json`, and libraries that are + // for some reason broken when imported through Webpack. If you just want to + // use an image, put it in `src` and `import` it from JavaScript instead. + contentBase: paths.appPublic, + // By default files from `contentBase` will not trigger a page reload. + watchContentBase: true, + // Enable hot reloading server. It will provide /sockjs-node/ endpoint + // for the WebpackDevServer client so it can learn when the files were + // updated. The WebpackDevServer client is included as an entry point + // in the Webpack development configuration. Note that only changes + // to CSS are currently hot reloaded. JS changes will refresh the browser. + hot: true, + // It is important to tell WebpackDevServer to use the same "root" path + // as we specified in the config. In development, we always serve from /. + publicPath: config.output.publicPath, + // WebpackDevServer is noisy by default so we emit custom message instead + // by listening to the compiler events with `compiler.plugin` calls above. + quiet: true, + // Reportedly, this avoids CPU overload on some systems. + // https://github.com/facebookincubator/create-react-app/issues/293 + watchOptions: { + ignored: /node_modules/, + }, + // Enable HTTPS if the HTTPS environment variable is set to 'true' + https: protocol === 'https', + host: host, + overlay: false, + historyApiFallback: { + // Paths with dots should still use the history fallback. + // See https://github.com/facebookincubator/create-react-app/issues/387. + disableDotRule: true, + }, + public: allowedHost, + proxy, + setup(app) { + // This lets us open files from the runtime error overlay. + app.use(errorOverlayMiddleware()) + // This service worker file is effectively a 'no-op' that will reset any + // previous service worker registered for the same host:port combination. + // We do this in development to avoid hitting the production cache if + // it used the same host and port. + // https://github.com/facebookincubator/create-react-app/issues/2272#issuecomment-302832432 + app.use(noopServiceWorkerMiddleware()) + }, + } +} diff --git a/scripts/start.js b/scripts/start.js index a079e48d..72fea05d 100644 --- a/scripts/start.js +++ b/scripts/start.js @@ -7,164 +7,168 @@ const chalk = require('chalk') const webpack = require('webpack') const WebpackDevServer = require('webpack-dev-server') const config = require('../config/webpack.config.dev') +const {choosePort, prepareUrls, prepareProxy} = require('react-dev-utils/WebpackDevServerUtils') const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages') const clearConsole = require('react-dev-utils/clearConsole') const openBrowser = require('react-dev-utils/openBrowser') -const historyApiFallback = require('connect-history-api-fallback') -const httpProxyMiddleware = require('http-proxy-middleware') +const createDevServerConfig = require('../config/webpackDevServer.config') +const paths = require('../config/paths') if (fs.existsSync('elm-package.json') === false) { console.log('Please, run the build script from project root directory') process.exit(0) } -// http://webpack.github.io/docs/node.js-api.html#the-long-way -const compiler = webpack(config) -const port = 3000 +const isInteractive = process.stdout.isTTY -compiler.plugin('invalid', function () { - clearConsole() - console.log('Compiling...') -}) +function printInstructions (appName, urls) { + console.log() + console.log(`You can now view ${chalk.bold(appName)} in the browser.`) + console.log() -compiler.plugin('done', function (stats) { - clearConsole() - - const hasErrors = stats.hasErrors() - const hasWarnings = stats.hasWarnings() - - if (!hasErrors && !hasWarnings) { - console.log(chalk.green('Compiled successfully!')) - console.log('\nThe app is running at:') - console.log('\n ' + chalk.cyan('http://localhost:' + port + '/')) - console.log('\nTo create production build, run:') - console.log('\n elm-app build') - return - } - - if (hasErrors) { - console.log(chalk.red('Compilation failed.\n')) - - const json = formatWebpackMessages(stats.toJson({}, true)) - - json.errors.forEach(function (message) { - console.log(message) - console.log() - }) - } -}) - -// We need to provide a custom onError function for httpProxyMiddleware. -// It allows us to log custom error messages on the console. -function onProxyError (proxy) { - return function (err, req, res) { - const host = req.headers && req.headers.host + if (urls.lanUrlForTerminal) { console.log( - chalk.red('Proxy error:') + ' Could not proxy request ' + chalk.cyan(req.url) + - ' from ' + chalk.cyan(host) + ' to ' + chalk.cyan(proxy) + '.' + ` ${chalk.bold('Local:')} ${urls.localUrlForTerminal}` ) console.log( - 'See https://nodejs.org/api/errors.html#errors_common_system_errors for more information (' + - chalk.cyan(err.code) + ').' - ) - console.log() - - // And immediately send the proper error response to the client. - // Otherwise, the request will eventually timeout with ERR_EMPTY_RESPONSE on the client side. - if (res.writeHead && !res.headersSent) { - res.writeHead(500) - } - res.end('Proxy error: Could not proxy request ' + req.url + ' from ' + - host + ' to ' + proxy + ' (' + err.code + ').' + ` ${chalk.bold('On Your Network:')} ${urls.lanUrlForTerminal}` ) + } else { + console.log(` ${urls.localUrlForTerminal}`) } + + console.log() + console.log('Note that the development build is not optimized.') + console.log( + `To create a production build, use ` + + `${chalk.cyan('elm-app build')}.` + ) + console.log() } -function addMiddleware (devServer) { - // `proxy` lets you to specify a fallback server during development. - // Every unrecognized request will be forwarded to it. - const proxy = JSON.parse(fs.readFileSync('elm-package.json', 'utf-8')).proxy - devServer.use(historyApiFallback({ - // Paths with dots should still use the history fallback. - // See https://github.com/facebookincubator/create-react-app/issues/387. - disableDotRule: true, - // For single page apps, we generally want to fallback to /index.html. - // However we also want to respect `proxy` for API calls. - // So if `proxy` is specified, we need to decide which fallback to use. - // We use a heuristic: if request `accept`s text/html, we pick /index.html. - // Modern browsers include text/html into `accept` header when navigating. - // However API calls like `fetch()` won’t generally accept text/html. - // If this heuristic doesn’t work well for you, don’t use `proxy`. - htmlAcceptHeaders: proxy - ? [ 'text/html' ] - : [ 'text/html', '*/*' ] - })) - if (proxy) { - if (typeof proxy !== 'string') { - console.log(chalk.red('When specified, "proxy" in package.json must be a string.')) - console.log(chalk.red('Instead, the type of "proxy" was "' + typeof proxy + '".')) - console.log(chalk.red('Either remove "proxy" from package.json, or make it a string.')) - process.exit(1) - } +function createCompiler (webpack, config, appName, urls) { + // "Compiler" is a low-level interface to Webpack. + // It lets us listen to some events and provide our own custom messages. + let compiler + try { + compiler = webpack(config) + } catch (err) { + console.log(chalk.red('Failed to compile.')) + console.log() + console.log(err.message || err) + console.log() + process.exit(1) + } - // Otherwise, if proxy is specified, we will let it handle any request. - // There are a few exceptions which we won't send to the proxy: - // - /index.html (served as HTML5 history API fallback) - // - /*.hot-update.json (WebpackDevServer uses this too for hot reloading) - // - /sockjs-node/* (WebpackDevServer uses this for hot reloading) - // Tip: use https://jex.im/regulex/ to visualize the regex - const mayProxy = /^(?!\/(index\.html$|.*\.hot-update\.json$|sockjs-node\/)).*$/ - - // Pass the scope regex both to Express and to the middleware for proxying - // of both HTTP and WebSockets to work without false positives. - const hpm = httpProxyMiddleware( - function (pathname) { - return mayProxy.test(pathname) - }, - { - target: proxy, - logLevel: 'silent', - onProxyReq: function (proxyReq) { - // Browers may send Origin headers even with same-origin - // requests. To prevent CORS issues, we have to change - // the Origin to match the target URL. - if (proxyReq.getHeader('origin')) { - proxyReq.setHeader('origin', proxy) - } - }, - onError: onProxyError(proxy), - secure: false, - changeOrigin: true, - ws: true - }) - devServer.use(mayProxy, hpm) + // "invalid" event fires when you have changed a file, and Webpack is + // recompiling a bundle. WebpackDevServer takes care to pause serving the + // bundle, so if you refresh, it'll wait instead of serving the old one. + // "invalid" is short for "bundle invalidated", it doesn't imply any errors. + compiler.plugin('invalid', () => { + if (isInteractive) { + clearConsole() + } + console.log('Compiling...') + }) - // Listen for the websocket 'upgrade' event and upgrade the connection. - // If this is not done, httpProxyMiddleware will not try to upgrade until - // an initial plain HTTP request is made. - devServer.listeningApp.on('upgrade', hpm.upgrade) - } + let isFirstCompile = true - // Finally, by now we have certainly resolved the URL. - // It may be /index.html, so let the dev server try serving it again. - devServer.use(devServer.middleware) -} + // "done" event fires when Webpack has finished recompiling the bundle. + // Whether or not you have warnings or errors, you will get this event. + compiler.plugin('done', stats => { + if (isInteractive) { + clearConsole() + } -const devServer = new WebpackDevServer(compiler, { - hot: true, - inline: true, - publicPath: '/', - quiet: true, - historyApiFallback: true -}) + // We have switched off the default Webpack output in WebpackDevServer + // options so we are going to "massage" the warnings and errors and present + // them in a readable focused way. + const messages = formatWebpackMessages(stats.toJson({}, true)) + const isSuccessful = !messages.errors.length && !messages.warnings.length + if (isSuccessful) { + console.log(chalk.green('Compiled successfully!')) + } + if (isSuccessful && (isInteractive || isFirstCompile)) { + printInstructions(appName, urls) + } + isFirstCompile = false -addMiddleware(devServer) + // If errors exist, only show errors. + if (messages.errors.length) { + console.log(chalk.red('Failed to compile.\n')) + console.log(messages.errors.join('\n\n')) + return + } -// Launch WebpackDevServer. -devServer.listen(port, function (err) { - if (err) { - return console.log(err) - } -}) + // Show warnings if no errors were found. + if (messages.warnings.length) { + console.log(chalk.yellow('Compiled with warnings.\n')) + console.log(messages.warnings.join('\n\n')) + + // Teach some ESLint tricks. + console.log( + '\nSearch for the ' + + chalk.underline(chalk.yellow('keywords')) + + ' to learn more about each warning.' + ) + console.log( + 'To ignore, add ' + + chalk.cyan('// eslint-disable-next-line') + + ' to the line before.\n' + ) + } + }) + return compiler +} -openBrowser('http://localhost:' + port + '/') +// Tools like Cloud9 rely on this. +const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000 +const HOST = process.env.HOST || '0.0.0.0' + +// We attempt to use the default port but if it is busy, we offer the user to +// run on a different port. `detect()` Promise resolves to the next free port. +choosePort(HOST, DEFAULT_PORT) + .then(port => { + if (port == null) { + // We have not found a port. + return + } + const protocol = process.env.HTTPS === 'true' ? 'https' : 'http' + const appName = require(paths.elmPkg).name + const urls = prepareUrls(protocol, HOST, port) + // Create a webpack compiler that is configured with custom messages. + const compiler = createCompiler(webpack, config, appName, urls) + // Load proxy config + const proxySetting = require(paths.elmPkg).proxy + const proxyConfig = prepareProxy(proxySetting, '/') + // Serve webpack assets generated by the compiler over a web sever. + const serverConfig = createDevServerConfig( + proxyConfig, + urls.lanUrlForConfig + ) + const devServer = new WebpackDevServer(compiler, serverConfig) + // Launch WebpackDevServer. + devServer.listen(port, HOST, err => { + if (err) { + return console.log(err) + } + if (isInteractive) { + clearConsole() + } + console.log(chalk.cyan('Starting the development server...\n')) + openBrowser(urls.localUrlForBrowser) + }); + + ['SIGINT', 'SIGTERM'].forEach(function (sig) { + process.on(sig, function () { + devServer.close() + process.exit() + }) + }) + }) + .catch(err => { + if (err && err.message) { + console.log(err.message) + } + process.exit(1) + })