diff --git a/packages/react-dev-utils/WebpackDevServerUtils.js b/packages/react-dev-utils/WebpackDevServerUtils.js index 1208d7fc776..260f73ceea0 100644 --- a/packages/react-dev-utils/WebpackDevServerUtils.js +++ b/packages/react-dev-utils/WebpackDevServerUtils.js @@ -34,20 +34,20 @@ if (isSmokeTest) { }; } -function prepareUrls(protocol, host, port) { +function prepareUrls(protocol, host, port, pathname) { const formatUrl = hostname => url.format({ protocol, hostname, port, - pathname: '/', + pathname, }); const prettyPrintUrl = hostname => url.format({ protocol, hostname, port: chalk.bold(port), - pathname: '/', + pathname, }); const isUnspecifiedHost = host === '0.0.0.0' || host === '::'; diff --git a/packages/react-dev-utils/errorOverlayMiddleware.js b/packages/react-dev-utils/errorOverlayMiddleware.js index 873b1994732..57a6003e45d 100644 --- a/packages/react-dev-utils/errorOverlayMiddleware.js +++ b/packages/react-dev-utils/errorOverlayMiddleware.js @@ -9,9 +9,9 @@ const launchEditor = require('./launchEditor'); const launchEditorEndpoint = require('./launchEditorEndpoint'); -module.exports = function createLaunchEditorMiddleware() { +module.exports = function createLaunchEditorMiddleware(servedPathPathname) { return function launchEditorMiddleware(req, res, next) { - if (req.url.startsWith(launchEditorEndpoint)) { + if (req.url.startsWith(`${servedPathPathname}${launchEditorEndpoint}`)) { const lineNumber = parseInt(req.query.lineNumber, 10) || 1; const colNumber = parseInt(req.query.colNumber, 10) || 1; launchEditor(req.query.fileName, lineNumber, colNumber); diff --git a/packages/react-dev-utils/noopServiceWorkerMiddleware.js b/packages/react-dev-utils/noopServiceWorkerMiddleware.js index 568ff4d6519..ad32c09b230 100644 --- a/packages/react-dev-utils/noopServiceWorkerMiddleware.js +++ b/packages/react-dev-utils/noopServiceWorkerMiddleware.js @@ -7,9 +7,11 @@ 'use strict'; -module.exports = function createNoopServiceWorkerMiddleware() { +module.exports = function createNoopServiceWorkerMiddleware( + servedPathPathname +) { return function noopServiceWorkerMiddleware(req, res, next) { - if (req.url === '/service-worker.js') { + if (req.url === `${servedPathPathname}/service-worker.js`) { res.setHeader('Content-Type', 'text/javascript'); res.send( `// This service worker file is effectively a 'no-op' that will reset any diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json index 48197f822c3..8f40e19955e 100644 --- a/packages/react-dev-utils/package.json +++ b/packages/react-dev-utils/package.json @@ -16,6 +16,7 @@ "clearConsole.js", "crossSpawn.js", "errorOverlayMiddleware.js", + "serveAppMiddleware.js", "eslintFormatter.js", "FileSizeReporter.js", "formatWebpackMessages.js", diff --git a/packages/react-dev-utils/serveAppMiddleware.js b/packages/react-dev-utils/serveAppMiddleware.js new file mode 100644 index 00000000000..c2824a96d32 --- /dev/null +++ b/packages/react-dev-utils/serveAppMiddleware.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +'use strict'; + +module.exports = function createServeAppMiddleware(servedPathPathname) { + return function serveAppMiddleware(req, res, next) { + if (servedPathPathname.length > 1 && servedPathPathname !== './') { + if (req.url.indexOf(servedPathPathname) === -1) { + res.redirect(servedPathPathname); + } else { + next(); + } + } else { + next(); + } + }; +}; diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js index 7a1b66803bc..8bf6ea2bac5 100644 --- a/packages/react-scripts/config/webpack.config.dev.js +++ b/packages/react-scripts/config/webpack.config.dev.js @@ -10,6 +10,7 @@ const autoprefixer = require('autoprefixer'); const path = require('path'); +const url = require('url'); const webpack = require('webpack'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); @@ -22,14 +23,18 @@ const getClientEnvironment = require('./env'); const paths = require('./paths'); // Webpack uses `publicPath` to determine where the app is being served from. -// In development, we always serve from the root. This makes config easier. -const publicPath = '/'; +// In development, we serve from the root by default. Webpack will serve from +// the relative path of the homepage field if specified. +let publicPath = url.parse(paths.servedPath).pathname || ''; +if (publicPath === './') { + publicPath = publicPath.slice(1); +} // `publicUrl` is just like `publicPath`, but we will provide it to our app // as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript. // Omit trailing slash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz. -const publicUrl = ''; +const publicUrl = paths.servedPath.slice(0, -1) + '/static'; // Get environment variables to inject into our app. -const env = getClientEnvironment(publicUrl); +const env = getClientEnvironment(publicUrl === '.' ? '' : publicUrl); // Options for PostCSS as we reference these options twice // Adds vendor prefixing based on your specified browser support in @@ -111,6 +116,7 @@ module.exports = { // There are also additional JS chunk files if you use code splitting. chunkFilename: 'static/js/[name].chunk.js', // This is the URL that app is served from. We use "/" in development. + // If there is a homepage path defined, it will be served from that instead. publicPath: publicPath, // Point sourcemap entries to original disk location (format as URL on Windows) devtoolModuleFilenameTemplate: info => diff --git a/packages/react-scripts/config/webpackDevServer.config.js b/packages/react-scripts/config/webpackDevServer.config.js index 74119d27f39..69fe86d8448 100644 --- a/packages/react-scripts/config/webpackDevServer.config.js +++ b/packages/react-scripts/config/webpackDevServer.config.js @@ -10,12 +10,16 @@ const errorOverlayMiddleware = require('react-dev-utils/errorOverlayMiddleware'); const noopServiceWorkerMiddleware = require('react-dev-utils/noopServiceWorkerMiddleware'); +const serveAppMiddleware = require('react-dev-utils/serveAppMiddleware'); const ignoredFiles = require('react-dev-utils/ignoredFiles'); +const url = require('url'); const config = require('./webpack.config.dev'); const paths = require('./paths'); +const express = require('express'); const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; const host = process.env.HOST || '0.0.0.0'; +const servedPathPathname = url.parse(paths.servedPath).pathname || ''; module.exports = function(proxy, allowedHost) { return { @@ -86,18 +90,26 @@ module.exports = function(proxy, allowedHost) { // Paths with dots should still use the history fallback. // See https://github.com/facebook/create-react-app/issues/387. disableDotRule: true, + index: servedPathPathname, }, public: allowedHost, proxy, before(app) { // This lets us open files from the runtime error overlay. - app.use(errorOverlayMiddleware()); + app.use(errorOverlayMiddleware(servedPathPathname)); // 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/facebook/create-react-app/issues/2272#issuecomment-302832432 - app.use(noopServiceWorkerMiddleware()); + app.use(noopServiceWorkerMiddleware(servedPathPathname)); + // serves the app up from the homepage if specified + app.use(serveAppMiddleware(servedPathPathname)); + // serve up static assets + app.use( + `${config.output.publicPath.slice(0, -1)}/static`, + express.static(paths.appPublic) + ); }, }; }; diff --git a/packages/react-scripts/fixtures/kitchensink/integration/env.test.js b/packages/react-scripts/fixtures/kitchensink/integration/env.test.js index 43badcbde8e..ee7f27c383f 100644 --- a/packages/react-scripts/fixtures/kitchensink/integration/env.test.js +++ b/packages/react-scripts/fixtures/kitchensink/integration/env.test.js @@ -50,7 +50,7 @@ describe('Integration', () => { const prefix = process.env.NODE_ENV === 'development' - ? '' + ? '/static' : 'http://www.example.org/spa'; expect(doc.getElementById('feature-public-url').textContent).to.equal( `${prefix}.` diff --git a/packages/react-scripts/scripts/start.js b/packages/react-scripts/scripts/start.js index 95502bc2a24..127cc4b4c92 100644 --- a/packages/react-scripts/scripts/start.js +++ b/packages/react-scripts/scripts/start.js @@ -29,6 +29,7 @@ if (process.env.SKIP_PREFLIGHT_CHECK !== 'true') { } // @remove-on-eject-end +const url = require('url'); const chalk = require('chalk'); const webpack = require('webpack'); const WebpackDevServer = require('webpack-dev-server'); @@ -56,6 +57,8 @@ if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; const HOST = process.env.HOST || '0.0.0.0'; +const servedPathPathname = url.parse(paths.servedPath).pathname || ''; + if (process.env.HOST) { console.log( chalk.cyan( @@ -89,7 +92,7 @@ checkBrowsers(paths.appPath) } const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; const appName = require(paths.appPackageJson).name; - const urls = prepareUrls(protocol, HOST, port); + const urls = prepareUrls(protocol, HOST, port, servedPathPathname); // Create a webpack compiler that is configured with custom messages. const compiler = createCompiler( webpack, diff --git a/packages/react-scripts/template/README.md b/packages/react-scripts/template/README.md index 21cf719e961..c30d9031817 100644 --- a/packages/react-scripts/template/README.md +++ b/packages/react-scripts/template/README.md @@ -2192,6 +2192,13 @@ To override this, specify the `homepage` in your `package.json`, for example: This will let Create React App correctly infer the root path to use in the generated HTML file. +If `homepage` is specified, Create React App will open your browser at the path specified. From the example above, `npm start` would result in: + +```js +http://localhost:3000/relativepath +``` +This also means that in development the paths to the static files will be served out of the `relativepath` directory. + **Note**: If you are using `react-router@^4`, you can root ``s using the `basename` prop on any ``.
More information [here](https://reacttraining.com/react-router/web/api/BrowserRouter/basename-string).