diff --git a/packages/react-scripts/config/paths.js b/packages/react-scripts/config/paths.js index 2f10ea2fb8a..567ee0536ea 100644 --- a/packages/react-scripts/config/paths.js +++ b/packages/react-scripts/config/paths.js @@ -11,6 +11,7 @@ var path = require('path'); var fs = require('fs'); +var url = require('url'); // Make sure any symlinks in the project folder are resolved: // https://github.com/facebookincubator/create-react-app/issues/637 @@ -40,6 +41,28 @@ var nodePaths = (process.env.NODE_PATH || '') .filter(folder => !path.isAbsolute(folder)) .map(resolveApp); +var envPublicUrl = process.env.PUBLIC_URL; + +function getPublicUrl(appPackageJson) { + return envPublicUrl || require(appPackageJson).homepage; +} + +// We use `PUBLIC_URL` environment variable or "homepage" field to infer +// "public path" at which the app is served. +// Webpack needs to know it to put the right <script> hrefs into HTML even in +// single-page apps that may serve index.html for nested URLs like /todos/42. +// We can't use a relative path in HTML because we don't want to load something +// like /todos/42/static/js/bundle.7289d.js. We have to know the root. +function getServedPath(appPackageJson) { + var publicUrl = getPublicUrl(appPackageJson) + if (!publicUrl) { + return '/' + } else if (envPublicUrl) { + return publicUrl; + } + return url.parse(publicUrl).pathname; +} + // config after eject: we're in ./config/ module.exports = { appBuild: resolveApp('build'), @@ -52,7 +75,9 @@ module.exports = { testsSetup: resolveApp('src/setupTests.js'), appNodeModules: resolveApp('node_modules'), ownNodeModules: resolveApp('node_modules'), - nodePaths: nodePaths + nodePaths: nodePaths, + publicUrl: getPublicUrl(resolveApp('package.json')), + servedPath: getServedPath(resolveApp('package.json')) }; // @remove-on-eject-begin @@ -73,7 +98,9 @@ module.exports = { appNodeModules: resolveApp('node_modules'), // this is empty with npm3 but node resolution searches higher anyway: ownNodeModules: resolveOwn('../node_modules'), - nodePaths: nodePaths + nodePaths: nodePaths, + publicUrl: getPublicUrl(resolveApp('package.json')), + servedPath: getServedPath(resolveApp('package.json')) }; // config before publish: we're in ./packages/react-scripts/config/ @@ -89,7 +116,9 @@ if (__dirname.indexOf(path.join('packages', 'react-scripts', 'config')) !== -1) testsSetup: resolveOwn('../template/src/setupTests.js'), appNodeModules: resolveOwn('../node_modules'), ownNodeModules: resolveOwn('../node_modules'), - nodePaths: nodePaths + nodePaths: nodePaths, + publicUrl: getPublicUrl(resolveOwn('../package.json')), + servedPath: getServedPath(resolveOwn('../package.json')) }; } // @remove-on-eject-end diff --git a/packages/react-scripts/config/utils/ensureSlash.js b/packages/react-scripts/config/utils/ensureSlash.js new file mode 100644 index 00000000000..f4382384f7e --- /dev/null +++ b/packages/react-scripts/config/utils/ensureSlash.js @@ -0,0 +1,10 @@ +module.exports = function ensureSlash(path, needsSlash) { + var hasSlash = path.endsWith('/'); + if (hasSlash && !needsSlash) { + return path.substr(path, path.length - 1); + } else if (!hasSlash && needsSlash) { + return path + '/'; + } else { + return path; + } +} diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js index 96fd632b795..47eea43fe84 100644 --- a/packages/react-scripts/config/webpack.config.dev.js +++ b/packages/react-scripts/config/webpack.config.dev.js @@ -15,6 +15,7 @@ var HtmlWebpackPlugin = require('html-webpack-plugin'); var CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); var InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin'); +var ensureSlash = require('./utils/ensureSlash'); var getClientEnvironment = require('./env'); var paths = require('./paths'); @@ -29,7 +30,7 @@ var publicPath = '/'; // `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. -var publicUrl = ''; +var publicUrl = ensureSlash(paths.servedPath, false); // Get environment variables to inject into our app. var env = getClientEnvironment(publicUrl); diff --git a/packages/react-scripts/config/webpack.config.prod.js b/packages/react-scripts/config/webpack.config.prod.js index 058db0d7921..f005c11f8ad 100644 --- a/packages/react-scripts/config/webpack.config.prod.js +++ b/packages/react-scripts/config/webpack.config.prod.js @@ -16,6 +16,7 @@ var ExtractTextPlugin = require('extract-text-webpack-plugin'); var ManifestPlugin = require('webpack-manifest-plugin'); var InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); var url = require('url'); +var ensureSlash = require('./utils/ensureSlash'); var paths = require('./paths'); var getClientEnvironment = require('./env'); @@ -24,31 +25,13 @@ var getClientEnvironment = require('./env'); var path = require('path'); // @remove-on-eject-end -function ensureSlash(path, needsSlash) { - var hasSlash = path.endsWith('/'); - if (hasSlash && !needsSlash) { - return path.substr(path, path.length - 1); - } else if (!hasSlash && needsSlash) { - return path + '/'; - } else { - return path; - } -} - -// We use "homepage" field to infer "public path" at which the app is served. -// Webpack needs to know it to put the right <script> hrefs into HTML even in -// single-page apps that may serve index.html for nested URLs like /todos/42. -// We can't use a relative path in HTML because we don't want to load something -// like /todos/42/static/js/bundle.7289d.js. We have to know the root. -var homepagePath = require(paths.appPackageJson).homepage; -var homepagePathname = homepagePath ? url.parse(homepagePath).pathname : '/'; // Webpack uses `publicPath` to determine where the app is being served from. // It requires a trailing slash, or the file assets will get an incorrect path. -var publicPath = ensureSlash(homepagePathname, true); +var publicPath = ensureSlash(paths.servedPath, true); // `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. -var publicUrl = ensureSlash(homepagePathname, false); +// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz. +var publicUrl = ensureSlash(paths.servedPath, false); // Get environment variables to inject into our app. var env = getClientEnvironment(publicUrl); diff --git a/packages/react-scripts/fixtures/kitchensink/integration/env.test.js b/packages/react-scripts/fixtures/kitchensink/integration/env.test.js index a179aa7cbb2..414229b22ac 100644 --- a/packages/react-scripts/fixtures/kitchensink/integration/env.test.js +++ b/packages/react-scripts/fixtures/kitchensink/integration/env.test.js @@ -3,22 +3,30 @@ import initDOM from './initDOM' describe('Integration', () => { describe('Environment variables', () => { + it('file env variables', async () => { + const doc = await initDOM('file-env-variables') + + expect(doc.getElementById('feature-file-env-variables').textContent).to.equal('fromtheenvfile.') + }) + it('NODE_PATH', async () => { const doc = await initDOM('node-path') expect(doc.getElementById('feature-node-path').childElementCount).to.equal(4) }) - it('shell env variables', async () => { - const doc = await initDOM('shell-env-variables') + it('PUBLIC_URL', async () => { + const doc = await initDOM('public-url') - expect(doc.getElementById('feature-shell-env-variables').textContent).to.equal('fromtheshell.') + expect(doc.getElementById('feature-public-url').textContent).to.equal('http://www.example.org/spa.') + expect(doc.querySelector('head link[rel="shortcut icon"]').getAttribute('href')) + .to.equal('http://www.example.org/spa/favicon.ico') }) - it('file env variables', async () => { - const doc = await initDOM('file-env-variables') + it('shell env variables', async () => { + const doc = await initDOM('shell-env-variables') - expect(doc.getElementById('feature-file-env-variables').textContent).to.equal('fromtheenvfile.') + expect(doc.getElementById('feature-shell-env-variables').textContent).to.equal('fromtheshell.') }) }) }) diff --git a/packages/react-scripts/fixtures/kitchensink/integration/initDOM.js b/packages/react-scripts/fixtures/kitchensink/integration/initDOM.js index cec02227493..7cf8134b79f 100644 --- a/packages/react-scripts/fixtures/kitchensink/integration/initDOM.js +++ b/packages/react-scripts/fixtures/kitchensink/integration/initDOM.js @@ -15,9 +15,11 @@ if (process.env.E2E_FILE) { const markup = fs.readFileSync(file, 'utf8') getMarkup = () => markup + const pathPrefix = process.env.PUBLIC_URL.replace(/^https?:\/\/[^\/]+\/?/, '') + resourceLoader = (resource, callback) => callback( null, - fs.readFileSync(path.join(path.dirname(file), resource.url.pathname), 'utf8') + fs.readFileSync(path.join(path.dirname(file), resource.url.pathname.replace(pathPrefix, '')), 'utf8') ) } else if (process.env.E2E_URL) { getMarkup = () => new Promise(resolve => { @@ -37,7 +39,7 @@ if (process.env.E2E_FILE) { export default feature => new Promise(async resolve => { const markup = await getMarkup() - const host = process.env.E2E_URL || 'http://localhost:3000' + const host = process.env.E2E_URL || 'http://www.example.org/spa:3000' const doc = jsdom.jsdom(markup, { features: { FetchExternalResources: ['script', 'css'], diff --git a/packages/react-scripts/fixtures/kitchensink/src/App.js b/packages/react-scripts/fixtures/kitchensink/src/App.js index cf93b4c132b..be9465ece36 100644 --- a/packages/react-scripts/fixtures/kitchensink/src/App.js +++ b/packages/react-scripts/fixtures/kitchensink/src/App.js @@ -32,7 +32,8 @@ class App extends React.Component { } componentDidMount() { - switch (location.hash.slice(1)) { + const feature = location.hash.slice(1) + switch (feature) { case 'array-destructuring': require.ensure([], () => this.setFeature(require('./features/syntax/ArrayDestructuring').default)); break; @@ -87,6 +88,9 @@ class App extends React.Component { case 'promises': require.ensure([], () => this.setFeature(require('./features/syntax/Promises').default)); break; + case 'public-url': + require.ensure([], () => this.setFeature(require('./features/env/PublicUrl').default)); + break; case 'rest-and-default': require.ensure([], () => this.setFeature(require('./features/syntax/RestAndDefault').default)); break; @@ -106,6 +110,9 @@ class App extends React.Component { require.ensure([], () => this.setFeature(require('./features/webpack/UnknownExtInclusion').default)); break; default: + if (feature) { + throw new Error(`Missing feature "${feature}"`); + } this.setFeature(null); break; } diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/env/PublicUrl.js b/packages/react-scripts/fixtures/kitchensink/src/features/env/PublicUrl.js new file mode 100644 index 00000000000..bbb7b958b61 --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/src/features/env/PublicUrl.js @@ -0,0 +1,5 @@ +import React from 'react' + +export default () => ( + <span id="feature-public-url">{process.env.PUBLIC_URL}.</span> +) diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/env/PublicUrl.test.js b/packages/react-scripts/fixtures/kitchensink/src/features/env/PublicUrl.test.js new file mode 100644 index 00000000000..31e699ece0b --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/src/features/env/PublicUrl.test.js @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import PublicUrl from './PublicUrl'; + +describe('PUBLIC_URL', () => { + it('renders without crashing', () => { + const div = document.createElement('div'); + ReactDOM.render(<PublicUrl />, div); + }); +}); diff --git a/packages/react-scripts/scripts/build.js b/packages/react-scripts/scripts/build.js index 42be50d43a8..873069c0dc6 100644 --- a/packages/react-scripts/scripts/build.js +++ b/packages/react-scripts/scripts/build.js @@ -21,6 +21,7 @@ require('dotenv').config({silent: true}); var chalk = require('chalk'); var fs = require('fs-extra'); var path = require('path'); +var url = require('url'); var filesize = require('filesize'); var gzipSize = require('gzip-size').sync; var webpack = require('webpack'); @@ -158,15 +159,16 @@ function build(previousSizeMap) { var openCommand = process.platform === 'win32' ? 'start' : 'open'; var appPackage = require(paths.appPackageJson); - var homepagePath = appPackage.homepage; + var publicUrl = paths.publicUrl; var publicPath = config.output.publicPath; - if (homepagePath && homepagePath.indexOf('.github.io/') !== -1) { + var publicPathname = url.parse(publicPath).pathname; + if (publicUrl && publicUrl.indexOf('.github.io/') !== -1) { // "homepage": "http://user.github.io/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('The project was built assuming it is hosted at ' + chalk.green(publicPathname) + '.'); + console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + ', or with the ' + chalk.green('PUBLIC_URL') + ' environment variable.'); console.log(); console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.'); - console.log('To publish it at ' + chalk.green(homepagePath) + ', run:'); + console.log('To publish it at ' + chalk.green(publicUrl) + ', run:'); // If script deploy has been added to package.json, skip the instructions if (typeof appPackage.scripts.deploy === 'undefined') { console.log(); @@ -193,20 +195,20 @@ function build(previousSizeMap) { } else 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('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + ', or with the ' + chalk.green('PUBLIC_URL') + ' environment variable.'); console.log(); console.log('The ' + chalk.cyan('build') + ' 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) { + if (publicUrl) { // "homepage": "http://mywebsite.com" - console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.'); + console.log('The project was built assuming it is hosted at ' + chalk.green(publicUrl) + '.'); + console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + ', or with the ' + chalk.green('PUBLIC_URL') + ' environment variable.'); console.log(); } else { // no homepage - console.log('To override this, specify the ' + chalk.green('homepage') + ' in your ' + chalk.cyan('package.json') + '.'); + console.log('The project was built assuming it is hosted at the server root.'); + console.log('To override this, specify the ' + chalk.green('homepage') + ' in your ' + chalk.cyan('package.json') + ', or with the ' + chalk.green('PUBLIC_URL') + ' environment variable.'); console.log('For example, add this to build it for GitHub Pages:') console.log(); console.log(' ' + chalk.green('"homepage"') + chalk.cyan(': ') + chalk.green('"http://myname.github.io/myapp"') + chalk.cyan(',')); diff --git a/packages/react-scripts/scripts/eject.js b/packages/react-scripts/scripts/eject.js index b8f9d313143..daa5958e9f9 100644 --- a/packages/react-scripts/scripts/eject.js +++ b/packages/react-scripts/scripts/eject.js @@ -43,12 +43,6 @@ prompt( } } - var folders = [ - 'config', - path.join('config', 'jest'), - 'scripts' - ]; - var files = [ path.join('config', 'env.js'), path.join('config', 'paths.js'), @@ -57,11 +51,20 @@ prompt( path.join('config', 'webpack.config.prod.js'), path.join('config', 'jest', 'cssTransform.js'), path.join('config', 'jest', 'fileTransform.js'), + path.join('config', 'utils', 'ensureSlash.js'), path.join('scripts', 'build.js'), path.join('scripts', 'start.js'), - path.join('scripts', 'test.js') + path.join('scripts', 'test.js'), ]; + var folders = files.reduce(function(prevFolders, file) { + var dirname = path.dirname(file); + if (prevFolders.indexOf(dirname) === -1) { + return prevFolders.concat(dirname); + } + return prevFolders; + }, []); + // Ensure that the app folder is clean and we won't override any files folders.forEach(verifyAbsent); files.forEach(verifyAbsent); diff --git a/packages/react-scripts/scripts/start.js b/packages/react-scripts/scripts/start.js index 8615fb074a9..c326b9b94d9 100644 --- a/packages/react-scripts/scripts/start.js +++ b/packages/react-scripts/scripts/start.js @@ -241,7 +241,7 @@ function runDevServer(host, port, protocol) { // 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_PATH%: + // In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%: // <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> // 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 diff --git a/tasks/e2e-kitchensink.sh b/tasks/e2e-kitchensink.sh index 892230ab747..3db6ef8d1d0 100755 --- a/tasks/e2e-kitchensink.sh +++ b/tasks/e2e-kitchensink.sh @@ -111,7 +111,11 @@ create_react_app --scripts-version=$scripts_path --internal-testing-template=$ro cd test-kitchensink # Test the build -NODE_PATH=src REACT_APP_SHELL_ENV_MESSAGE=fromtheshell npm run build +REACT_APP_SHELL_ENV_MESSAGE=fromtheshell \ + NODE_PATH=src \ + PUBLIC_URL=http://www.example.org/spa/ \ + npm run build + # Check for expected output test -e build/*.html test -e build/static/js/main.*.js @@ -127,6 +131,7 @@ tmp_server_log=`mktemp` PORT=3001 \ REACT_APP_SHELL_ENV_MESSAGE=fromtheshell \ NODE_PATH=src \ + PUBLIC_URL=http://www.example.org/spa/ \ nohup npm start &>$tmp_server_log & grep -q 'The app is running at:' <(tail -f $tmp_server_log) E2E_URL="http://localhost:3001" \ @@ -138,6 +143,7 @@ E2E_URL="http://localhost:3001" \ E2E_FILE=./build/index.html \ CI=true \ NODE_PATH=src \ + PUBLIC_URL=http://www.example.org/spa/ \ node_modules/.bin/mocha --require babel-register --require babel-polyfill integration/*.test.js # ****************************************************************************** @@ -157,7 +163,11 @@ npm link $root_path/packages/react-scripts rm .babelrc # Test the build -NODE_PATH=src REACT_APP_SHELL_ENV_MESSAGE=fromtheshell npm run build +REACT_APP_SHELL_ENV_MESSAGE=fromtheshell \ + NODE_PATH=src \ + PUBLIC_URL=http://www.example.org/spa/ \ + npm run build + # Check for expected output test -e build/*.html test -e build/static/js/main.*.js @@ -173,6 +183,7 @@ tmp_server_log=`mktemp` PORT=3002 \ REACT_APP_SHELL_ENV_MESSAGE=fromtheshell \ NODE_PATH=src \ + PUBLIC_URL=http://www.example.org/spa/ \ nohup npm start &>$tmp_server_log & grep -q 'The app is running at:' <(tail -f $tmp_server_log) E2E_URL="http://localhost:3002" \ @@ -186,6 +197,7 @@ E2E_FILE=./build/index.html \ CI=true \ NODE_ENV=production \ NODE_PATH=src \ + PUBLIC_URL=http://www.example.org/spa/ \ node_modules/.bin/mocha --require babel-register --require babel-polyfill integration/*.test.js # Cleanup