diff --git a/packages/react-scripts/config/env.js b/packages/react-scripts/config/env.js deleted file mode 100644 index 66acf119b8426a9ca3cee4b4a50fb54a96396fac..0000000000000000000000000000000000000000 --- a/packages/react-scripts/config/env.js +++ /dev/null @@ -1,26 +0,0 @@ -// @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 - -// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be -// injected into the application via DefinePlugin in Webpack configuration. - -var REACT_APP = /^REACT_APP_/i; -var NODE_ENV = JSON.stringify(process.env.NODE_ENV || 'development'); - -module.exports = Object - .keys(process.env) - .filter(key => REACT_APP.test(key)) - .reduce((env, key) => { - env['process.env.' + key] = JSON.stringify(process.env[key]); - return env; - }, { - 'process.env.NODE_ENV': NODE_ENV - }); diff --git a/packages/react-scripts/config/paths.js b/packages/react-scripts/config/paths.js index 314bcc2051f3dfa7b7626a8299f979b041d298e4..1d50c38b9f8da05f3329f4a10293cc72ddca99ab 100644 --- a/packages/react-scripts/config/paths.js +++ b/packages/react-scripts/config/paths.js @@ -38,7 +38,8 @@ var nodePaths = (process.env.NODE_PATH || '') // config after eject: we're in ./config/ module.exports = { appBuild: resolveApp('build'), - appHtml: resolveApp('index.html'), + appPublic: resolveApp('public'), + appHtml: resolveApp('public/index.html'), appIndexJs: resolveApp('src/index.js'), appPackageJson: resolveApp('package.json'), appSrc: resolveApp('src'), @@ -56,7 +57,8 @@ function resolveOwn(relativePath) { // config before eject: we're in ./node_modules/react-scripts/config/ module.exports = { appBuild: resolveApp('build'), - appHtml: resolveApp('index.html'), + appPublic: resolveApp('public'), + appHtml: resolveApp('public/index.html'), appIndexJs: resolveApp('src/index.js'), appPackageJson: resolveApp('package.json'), appSrc: resolveApp('src'), @@ -71,7 +73,8 @@ module.exports = { // @remove-on-publish-begin module.exports = { appBuild: resolveOwn('../../../build'), - appHtml: resolveOwn('../template/index.html'), + appPublic: resolveOwn('../template/public'), + appHtml: resolveOwn('../template/public/index.html'), appIndexJs: resolveOwn('../template/src/index.js'), appPackageJson: resolveOwn('../package.json'), appSrc: resolveOwn('../template/src'), diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js index 8d1de1c327da40256b429e4cc6e21751937b3df8..f08fcdcb7dc441580cbee8b53952a8a4a6c27029 100644 --- a/packages/react-scripts/config/webpack.config.dev.js +++ b/packages/react-scripts/config/webpack.config.dev.js @@ -14,9 +14,20 @@ var autoprefixer = require('autoprefixer'); var webpack = require('webpack'); var HtmlWebpackPlugin = require('html-webpack-plugin'); var CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); +var InterpolateHtmlPlugin = require('../scripts/utils/InterpolateHtmlPlugin'); var WatchMissingNodeModulesPlugin = require('../scripts/utils/WatchMissingNodeModulesPlugin'); +var getClientEnvironment = require('../scripts/utils/getClientEnvironment'); var paths = require('./paths'); -var env = require('./env'); + +// Webpack uses `publicPath` to determine where the app is being served from. +// In development, we always serve from the root. This makes config easier. +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 shlash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz. +var publicUrl = ''; +// Get enrivonment variables to inject into our app. +var env = getClientEnvironment(publicUrl); // This is the development configuration. // It is focused on developer experience and fast rebuilds. @@ -63,8 +74,8 @@ module.exports = { // served by WebpackDevServer in development. This is the JS bundle // containing code from all our entry points, and the Webpack runtime. filename: 'static/js/bundle.js', - // In development, we always serve from the root. This makes config easier. - publicPath: '/' + // This is the URL that app is served from. We use "/" in development. + publicPath: publicPath }, resolve: { // This allows you to set a fallback for where Webpack should look for modules. @@ -129,21 +140,11 @@ module.exports = { // In production, they would get copied to the `build` folder. { test: /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2)(\?.*)?$/, - exclude: /\/favicon.ico$/, loader: 'file', query: { name: 'static/media/[name].[hash:8].[ext]' } }, - // A special case for favicon.ico to place it into build root directory. - { - test: /\/favicon.ico$/, - include: [paths.appSrc], - loader: 'file', - query: { - name: 'favicon.ico?[hash:8]' - } - }, // "url" loader works just like "file" loader but it also embeds // assets smaller than specified size as data URLs to avoid requests. { @@ -153,15 +154,6 @@ module.exports = { limit: 10000, name: 'static/media/[name].[hash:8].[ext]' } - }, - // "html" loader is used to process template page (index.html) to resolve - // resources linked with <link href="./relative/path"> HTML tags. - { - test: /\.html$/, - loader: 'html', - query: { - attrs: ['link:href'], - } } ] }, @@ -186,13 +178,19 @@ module.exports = { ]; }, plugins: [ + // Makes the public URL available as %PUBLIC_URL% in index.html, e.g.: + // <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> + // In development, this will be an empty string. + new InterpolateHtmlPlugin({ + PUBLIC_URL: publicUrl + }), // Generates an `index.html` file with the <script> injected. new HtmlWebpackPlugin({ inject: true, template: paths.appHtml, }), // Makes some environment variables available to the JS code, for example: - // if (process.env.NODE_ENV === 'development') { ... }. See `env.js`. + // if (process.env.NODE_ENV === 'development') { ... }. new webpack.DefinePlugin(env), // This is necessary to emit hot updates (currently CSS only): new webpack.HotModuleReplacementPlugin(), diff --git a/packages/react-scripts/config/webpack.config.prod.js b/packages/react-scripts/config/webpack.config.prod.js index 5790e8e955182a162b9dee99a8f2a1ac40e33a91..3db7831be1ee34056542fe44b02f5a3d366f29fb 100644 --- a/packages/react-scripts/config/webpack.config.prod.js +++ b/packages/react-scripts/config/webpack.config.prod.js @@ -16,12 +16,18 @@ var HtmlWebpackPlugin = require('html-webpack-plugin'); var ExtractTextPlugin = require('extract-text-webpack-plugin'); var url = require('url'); var paths = require('./paths'); -var env = require('./env'); +var InterpolateHtmlPlugin = require('../scripts/utils/InterpolateHtmlPlugin'); +var getClientEnvironment = require('../scripts/utils/getClientEnvironment'); -// 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.'); +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. @@ -30,10 +36,21 @@ if (env['process.env.NODE_ENV'] !== '"production"') { // 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 publicPath = homepagePath ? url.parse(homepagePath).pathname : '/'; -if (!publicPath.endsWith('/')) { - // If we don't do this, file assets will get incorrect paths. - publicPath += '/'; +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); +// `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 shlash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz. +var publicUrl = ensureSlash(homepagePathname, false); +// Get enrivonment 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. @@ -139,21 +156,11 @@ module.exports = { // When you `import` an asset, you get its filename. { test: /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2)(\?.*)?$/, - exclude: /\/favicon.ico$/, loader: 'file', query: { name: 'static/media/[name].[hash:8].[ext]' } }, - // A special case for favicon.ico to place it into build root directory. - { - test: /\/favicon.ico$/, - include: [paths.appSrc], - loader: 'file', - query: { - name: 'favicon.ico?[hash:8]' - } - }, // "url" loader works just like "file" loader but it also embeds // assets smaller than specified size as data URLs to avoid requests. { @@ -163,15 +170,6 @@ module.exports = { limit: 10000, name: 'static/media/[name].[hash:8].[ext]' } - }, - // "html" loader is used to process template page (index.html) to resolve - // resources linked with <link href="./relative/path"> HTML tags. - { - test: /\.html$/, - loader: 'html', - query: { - attrs: ['link:href'], - } } ] }, @@ -198,6 +196,13 @@ module.exports = { ]; }, plugins: [ + // Makes the public URL available as %PUBLIC_URL% in index.html, e.g.: + // <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> + // In production, it will be an empty string unless you specify "homepage" + // in `package.json`, in which case it will be the pathname of that URL. + new InterpolateHtmlPlugin({ + PUBLIC_URL: publicUrl + }), // Generates an `index.html` file with the <script> injected. new HtmlWebpackPlugin({ inject: true, @@ -216,7 +221,7 @@ module.exports = { } }), // Makes some environment variables available to the JS code, for example: - // if (process.env.NODE_ENV === 'production') { ... }. See `env.js`. + // if (process.env.NODE_ENV === 'production') { ... }. // It is absolutely essential that NODE_ENV was set to production here. // Otherwise React will be compiled in the very slow development mode. new webpack.DefinePlugin(env), diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index 00be129a0c09e21cc3feadd4cdd2e02b9e87e898..bc0fe3270a31a78d1d5019dabd0a452a37b33d5c 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -46,7 +46,6 @@ "find-cache-dir": "0.1.1", "fs-extra": "0.30.0", "gzip-size": "3.0.0", - "html-loader": "0.4.3", "html-webpack-plugin": "2.22.0", "http-proxy-middleware": "0.17.1", "jest": "15.1.1", diff --git a/packages/react-scripts/scripts/build.js b/packages/react-scripts/scripts/build.js index 71dcc798bba4ee5f1ff4e499c3eaf72a1d51e1d6..a0805e601ac38d5ba116628ae65c2677a25b2679 100644 --- a/packages/react-scripts/scripts/build.js +++ b/packages/react-scripts/scripts/build.js @@ -13,7 +13,7 @@ process.env.NODE_ENV = 'production'; var chalk = require('chalk'); -var fs = require('fs'); +var fs = require('fs-extra'); var path = require('path'); var filesize = require('filesize'); var gzipSize = require('gzip-size').sync; @@ -70,6 +70,9 @@ recursive(paths.appBuild, (err, fileNames) => { // Start the webpack build build(previousSizeMap); + + // Merge with the public folder + copyPublicFolder(); }); // Print a detailed summary of build files. @@ -175,3 +178,10 @@ function build(previousSizeMap) { } }); } + +function copyPublicFolder() { + fs.copySync(paths.appPublic, paths.appBuild, { + dereference: true, + filter: file => file !== paths.appHtml + }); +} diff --git a/packages/react-scripts/scripts/eject.js b/packages/react-scripts/scripts/eject.js index a36e1d196009569cb24a695fe2ad820cd3fbe69c..dd18cfb6a2114f31f1024d42e14b83aa48f92554 100644 --- a/packages/react-scripts/scripts/eject.js +++ b/packages/react-scripts/scripts/eject.js @@ -46,6 +46,8 @@ prompt( path.join('scripts', 'start.js'), path.join('scripts', 'utils', 'checkRequiredFiles.js'), path.join('scripts', 'utils', 'chrome.applescript'), + path.join('scripts', 'utils', 'getClientEnvironment.js'), + path.join('scripts', 'utils', 'InterpolateHtmlPlugin.js'), path.join('scripts', 'utils', 'prompt.js'), path.join('scripts', 'utils', 'WatchMissingNodeModulesPlugin.js') ]; diff --git a/packages/react-scripts/scripts/start.js b/packages/react-scripts/scripts/start.js index b0290bfc492437b9b6332b9fc65656df52ae8d0c..191631694573ad3c64cd2d14d18c382d999633b3 100644 --- a/packages/react-scripts/scripts/start.js +++ b/packages/react-scripts/scripts/start.js @@ -255,23 +255,21 @@ function runDevServer(port, protocol) { // 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 also serves files from the current directory. - // This might be useful in legacy apps. However we already encourage people - // to use Webpack for importing assets in the code, so we don't need to - // additionally serve files by their filenames. Otherwise, even if it - // works in development, those files will be missing in production, unless - // we explicitly copy them. But even if we copy all the files into - // the build output (which doesn't seem to be wise because it may contain - // private information such as files with API keys, for example), we would - // still have a problem. Since the filenames would be the same every time, - // browsers would cache their content, and updating file content would not - // work correctly. This is easily solved by importing assets through Webpack - // because if it can then append content hashes to filenames in production, - // just like it does for JS and CSS. And because we configured "html" loader - // to be used for HTML files, even <link href="./src/something.png"> would - // get resolved correctly by Webpack and handled both in development and - // in production without actually serving it by that path. - contentBase: [], + // 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_PATH%: + // <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 + // 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, // 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 diff --git a/packages/react-scripts/scripts/test.js b/packages/react-scripts/scripts/test.js index d13aff38017a189f0e49ba3ca7b898ef1c64a3ed..9ff834bcd17e7ce12772d39c86ce3b116b9801b6 100644 --- a/packages/react-scripts/scripts/test.js +++ b/packages/react-scripts/scripts/test.js @@ -8,6 +8,7 @@ */ process.env.NODE_ENV = 'test'; +process.env.PUBLIC_URL = ''; const createJestConfig = require('./utils/createJestConfig'); const jest = require('jest'); diff --git a/packages/react-scripts/scripts/utils/InterpolateHtmlPlugin.js b/packages/react-scripts/scripts/utils/InterpolateHtmlPlugin.js new file mode 100644 index 0000000000000000000000000000000000000000..bcb3a78a775e7473b76498c448274b6e1bf527b5 --- /dev/null +++ b/packages/react-scripts/scripts/utils/InterpolateHtmlPlugin.js @@ -0,0 +1,43 @@ +// @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 + +// This Webpack plugin lets us interpolate custom variables into `index.html`. +// Usage: `new InterpolateHtmlPlugin({ 'MY_VARIABLE': 42 })` +// Then, you can use %MY_VARIABLE% in your `index.html`. + +// It works in tandem with HtmlWebpackPlugin. +// Learn more about creating plugins like this: +// https://github.com/ampedandwired/html-webpack-plugin#events + +'use strict'; + +class InterpolateHtmlPlugin { + constructor(replacements) { + this.replacements = replacements; + } + + apply(compiler) { + compiler.plugin('compilation', compilation => { + compilation.plugin('html-webpack-plugin-before-html-processing', + (data, callback) => { + // Run HTML through a series of user-specified string replacements. + Object.keys(this.replacements).forEach(key => { + const value = this.replacements[key]; + data.html = data.html.replace('%' + key + '%', value); + }); + callback(null, data); + } + ); + }); + } +} + +module.exports = InterpolateHtmlPlugin; diff --git a/packages/react-scripts/scripts/utils/getClientEnvironment.js b/packages/react-scripts/scripts/utils/getClientEnvironment.js new file mode 100644 index 0000000000000000000000000000000000000000..2bdc65afa153eaaac8307df1220dbe5e205c2d9e --- /dev/null +++ b/packages/react-scripts/scripts/utils/getClientEnvironment.js @@ -0,0 +1,38 @@ +// @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 + +// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be +// injected into the application via DefinePlugin in Webpack configuration. + +var REACT_APP = /^REACT_APP_/i; + +function getClientEnvironment(publicUrl) { + return Object + .keys(process.env) + .filter(key => REACT_APP.test(key)) + .reduce((env, key) => { + env['process.env.' + key] = JSON.stringify(process.env[key]); + return env; + }, { + // Useful for determining whether we’re running in production mode. + // Most importantly, it switches React into the correct mode. + 'process.env.NODE_ENV': JSON.stringify( + process.env.NODE_ENV || 'development' + ), + // Useful for resolving the correct path to static assets in `public`. + // For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />. + // This should only be used as an escape hatch. Normally you would put + // images into the `src` and `import` them in code to get their paths. + 'process.env.PUBLIC_URL': JSON.stringify(publicUrl) + }); +} + +module.exports = getClientEnvironment; diff --git a/packages/react-scripts/template/index.html b/packages/react-scripts/template/index.html deleted file mode 100644 index f89671553ebcf7a8e800e08a35d3807bccc762b0..0000000000000000000000000000000000000000 --- a/packages/react-scripts/template/index.html +++ /dev/null @@ -1,22 +0,0 @@ -<!doctype html> -<html lang="en"> - <head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <link rel="shortcut icon" href="./src/favicon.ico"> - <title>React App</title> - </head> - <body> - <div id="root"></div> - <!-- - This HTML file is a template. - If you open it directly in the browser, you will see an empty page. - - You can add webfonts, meta tags, or analytics to this file. - The build step will place the bundled scripts into the <body> tag. - - To begin the development, run `npm start` in this folder. - To create a production bundle, use `npm run build`. - --> - </body> -</html> diff --git a/packages/react-scripts/template/src/favicon.ico b/packages/react-scripts/template/public/favicon.ico similarity index 100% rename from packages/react-scripts/template/src/favicon.ico rename to packages/react-scripts/template/public/favicon.ico diff --git a/packages/react-scripts/template/public/index.html b/packages/react-scripts/template/public/index.html new file mode 100644 index 0000000000000000000000000000000000000000..c89bbf37a446f9cfa51751b01ab6e76473ce59b4 --- /dev/null +++ b/packages/react-scripts/template/public/index.html @@ -0,0 +1,31 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> + <!-- + Notice the use of %PUBLIC_URL% in the tag above. + It will be replaced with the URL of the `public` folder during the build. + Only files inside the `public` folder can be referenced from the HTML. + + Unlike "/favicon.ico" or "favico.ico", "%PUBLIC_URL%/favicon.ico" will + work correctly both with client-side routing and a non-root public URL. + Learn how to configure a non-root public URL by running `npm run build`. + --> + <title>React App</title> + </head> + <body> + <div id="root"></div> + <!-- + This HTML file is a template. + If you open it directly in the browser, you will see an empty page. + + You can add webfonts, meta tags, or analytics to this file. + The build step will place the bundled scripts into the <body> tag. + + To begin the development, run `npm start`. + To create a production bundle, use `npm run build`. + --> + </body> +</html>