diff --git a/.travis.yml b/.travis.yml index 99edd919888..2c60bc2674b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,5 +24,7 @@ matrix: include: - node_js: 0.10 env: TEST_SUITE=simple - - node_js: 6 - env: USE_YARN=yes TEST_SUITE=simple +# There's a weird Yarn/Lerna bug related to prerelease versions. +# TODO: reenable after we ship 1.0. +# - node_js: 6 +# env: USE_YARN=yes TEST_SUITE=simple diff --git a/README.md b/README.md index 4a0eeab567a..e9658d8585c 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,9 @@ It correctly bundles React in production mode and optimizes the build for the be The build is minified and the filenames include the hashes.
Your app is ready to be deployed! + ## User Guide @@ -162,6 +165,9 @@ Please refer to the [User Guide](https://github.com/facebookincubator/create-rea * Import CSS and image files directly from JavaScript. * Autoprefixed CSS, so you don’t need `-webkit` or other prefixes. * A `build` script to bundle JS, CSS, and images for production, with sourcemaps. + **The feature set is intentionally limited**. It doesn’t support advanced features such as server rendering or CSS modules. The tool is also **non-configurable** because it is hard to provide a cohesive experience and easy updates across a set of tools when the user can tweak anything. diff --git a/packages/eslint-config-react-app/index.js b/packages/eslint-config-react-app/index.js index 9d876042c85..42aa9db2cb0 100644 --- a/packages/eslint-config-react-app/index.js +++ b/packages/eslint-config-react-app/index.js @@ -235,6 +235,8 @@ module.exports = { ], // https://github.com/benmosher/eslint-plugin-import/tree/master/docs/rules + 'import/first': 'error', + 'import/no-amd': 'error', 'import/no-webpack-loader-syntax': 'error', // https://github.com/yannickcr/eslint-plugin-react/tree/master/docs/rules diff --git a/packages/react-dev-utils/ModuleScopePlugin.js b/packages/react-dev-utils/ModuleScopePlugin.js new file mode 100644 index 00000000000..fd70a2f408a --- /dev/null +++ b/packages/react-dev-utils/ModuleScopePlugin.js @@ -0,0 +1,68 @@ +/** + * 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. + */ + +'use strict'; + +const chalk = require('chalk'); +const path = require('path'); + +class ModuleScopePlugin { + constructor(appSrc) { + this.appSrc = appSrc; + } + + apply(resolver) { + const { appSrc } = this; + resolver.plugin('file', (request, callback) => { + // Unknown issuer, probably webpack internals + if (!request.context.issuer) { + return callback(); + } + if ( + // If this resolves to a node_module, we don't care what happens next + request.descriptionFileRoot.indexOf('/node_modules/') !== -1 || + request.descriptionFileRoot.indexOf('\\node_modules\\') !== -1 || + // Make sure this request was manual + !request.__innerRequest_request + ) { + return callback(); + } + // Resolve the issuer from our appSrc and make sure it's one of our files + // Maybe an indexOf === 0 would be better? + const relative = path.relative(appSrc, request.context.issuer); + // If it's not in src/ or a subdirectory, not our request! + if (relative[0] === '.') { + return callback(); + } + // Find path from src to the requested file + const requestRelative = path.relative( + appSrc, + path.resolve( + path.dirname(request.context.issuer), + request.__innerRequest_request + ) + ); + // Error if in a parent directory of src/ + if (requestRelative[0] === '.') { + callback( + new Error( + `You attempted to import ${chalk.cyan(request.__innerRequest_request)} which falls outside of the project ${chalk.cyan('src/')} directory. ` + + `Relative imports outside of ${chalk.cyan('src/')} are not supported. ` + + `You can either move it inside ${chalk.cyan('src/')}, or add a symlink to it from project's ${chalk.cyan('node_modules/')}.` + ), + request + ); + } else { + callback(); + } + }); + } +} + +module.exports = ModuleScopePlugin; diff --git a/packages/react-dev-utils/README.md b/packages/react-dev-utils/README.md index ef720449356..d65c43ec667 100644 --- a/packages/react-dev-utils/README.md +++ b/packages/react-dev-utils/README.md @@ -34,7 +34,7 @@ var publicUrl = '/my-custom-url'; module.exports = { output: { // ... - publicPath: publicUrl + '/' + publicPath: publicUrl + '/' }, // ... plugins: [ @@ -56,6 +56,30 @@ module.exports = { } ``` + +#### `new ModuleScopePlugin(appSrc: string)` + +This Webpack plugin ensures that relative imports from app's source directory don't reach outside of it. + +```js +var path = require('path'); +var ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin'); + + +module.exports = { + // ... + resolve: { + // ... + plugins: [ + new ModuleScopePlugin(paths.appSrc), + // ... + ], + // ... + }, + // ... +} +``` + #### `new WatchMissingNodeModulesPlugin(nodeModulesPath: string)` This Webpack plugin ensures `npm install ` forces a project rebuild.
@@ -112,7 +136,7 @@ console.log('Just cleared the screen!'); #### `eslintFormatter(results: Object): string` -This is our custom ESLint formatter that integrates well with Create React App console output. +This is our custom ESLint formatter that integrates well with Create React App console output.
You can use the default one instead if you prefer so. ```js @@ -228,6 +252,19 @@ if (openBrowser('http://localhost:3000')) { } ``` +#### `printHostingInstructions(appPackage: Object, publicUrl: string, publicPath: string, buildFolder: string, useYarn: boolean): void` + +Prints hosting instructions after the project is built. + +Pass your parsed `package.json` object as `appPackage`, your the URL where you plan to host the app as `publicUrl`, `output.publicPath` from your Webpack configuration as `publicPath`, the `buildFolder` name, and whether to `useYarn` in instructions. + +```js +const appPackage = require(paths.appPackageJson); +const publicUrl = paths.publicUrl; +const publicPath = config.output.publicPath; +printHostingInstructions(appPackage, publicUrl, publicPath, 'build', true); +``` + #### `webpackHotDevClient.js` This is an alternative client for [WebpackDevServer](https://github.com/webpack/webpack-dev-server) that shows a syntax error overlay. diff --git a/packages/react-dev-utils/ansiHTML.js b/packages/react-dev-utils/ansiHTML.js index c4e850736ac..5d3e792036c 100644 --- a/packages/react-dev-utils/ansiHTML.js +++ b/packages/react-dev-utils/ansiHTML.js @@ -15,18 +15,18 @@ var Anser = require('anser'); // var base00 = 'ffffff'; // Default Background var base01 = 'f5f5f5'; // Lighter Background (Used for status bars) // var base02 = 'c8c8fa'; // Selection Background -var base03 = '969896'; // Comments, Invisibles, Line Highlighting +var base03 = '6e6e6e'; // Comments, Invisibles, Line Highlighting // var base04 = 'e8e8e8'; // Dark Foreground (Used for status bars) var base05 = '333333'; // Default Foreground, Caret, Delimiters, Operators // var base06 = 'ffffff'; // Light Foreground (Not often used) // var base07 = 'ffffff'; // Light Background (Not often used) -var base08 = 'ed6a43'; // Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted +var base08 = '881280'; // Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted // var base09 = '0086b3'; // Integers, Boolean, Constants, XML Attributes, Markup Link Url // var base0A = '795da3'; // Classes, Markup Bold, Search Text Background -var base0B = '183691'; // Strings, Inherited Class, Markup Code, Diff Inserted -var base0C = '183691'; // Support, Regular Expressions, Escape Characters, Markup Quotes +var base0B = '1155cc'; // Strings, Inherited Class, Markup Code, Diff Inserted +var base0C = '994500'; // Support, Regular Expressions, Escape Characters, Markup Quotes // var base0D = '795da3'; // Functions, Methods, Attribute IDs, Headings -var base0E = 'a71d5d'; // Keywords, Storage, Selector, Markup Italic, Diff Changed +var base0E = 'c80000'; // Keywords, Storage, Selector, Markup Italic, Diff Changed // var base0F = '333333'; // Deprecated, Opening/Closing Embedded Language Tags e.g. // Map ANSI colors from what babel-code-frame uses to base16-github diff --git a/packages/react-dev-utils/eslintFormatter.js b/packages/react-dev-utils/eslintFormatter.js index a051b9c5b0e..c087734eadd 100644 --- a/packages/react-dev-utils/eslintFormatter.js +++ b/packages/react-dev-utils/eslintFormatter.js @@ -12,6 +12,8 @@ function isError(message) { function formatter(results) { let output = '\n'; + let hasErrors = false; + let reportContainsErrorRuleIDs = false; results.forEach(result => { let messages = result.messages; @@ -19,12 +21,14 @@ function formatter(results) { return; } - let hasErrors = false; messages = messages.map(message => { let messageType; if (isError(message)) { messageType = 'error'; hasErrors = true; + if (message.ruleId) { + reportContainsErrorRuleIDs = true; + } } else { messageType = 'warn'; } @@ -61,6 +65,19 @@ function formatter(results) { output += `${outputTable}\n\n`; }); + if (reportContainsErrorRuleIDs) { + // Unlike with warnings, we have to do it here. + // We have similar code in react-scripts for warnings, + // but warnings can appear in multiple files so we only + // print it once at the end. For errors, however, we print + // it here because we always show at most one error, and + // we can only be sure it's an ESLint error before exiting + // this function. + output += 'Search for the ' + + chalk.underline(chalk.red('rule keywords')) + + ' to learn more about each error.'; + } + return output; } diff --git a/packages/react-dev-utils/formatWebpackMessages.js b/packages/react-dev-utils/formatWebpackMessages.js index c490097bac6..5dfd099c65c 100644 --- a/packages/react-dev-utils/formatWebpackMessages.js +++ b/packages/react-dev-utils/formatWebpackMessages.js @@ -65,7 +65,8 @@ function formatMessage(message, isError) { lines[1] .replace("Cannot resolve 'file' or 'directory' ", '') .replace('Cannot resolve module ', '') - .replace('Error: ', ''), + .replace('Error: ', '') + .replace('[CaseSensitivePathsPlugin] ', ''), ]; } diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json index d52fe30fd86..c63502cd8b0 100644 --- a/packages/react-dev-utils/package.json +++ b/packages/react-dev-utils/package.json @@ -21,9 +21,11 @@ "getProcessForPort.js", "InterpolateHtmlPlugin.js", "launchEditor.js", + "ModuleScopePlugin.js", "openBrowser.js", "openChrome.applescript", "prepareProxy.js", + "printHostingInstructions.js", "WatchMissingNodeModulesPlugin.js", "webpackHotDevClient.js" ], diff --git a/packages/react-dev-utils/printHostingInstructions.js b/packages/react-dev-utils/printHostingInstructions.js new file mode 100644 index 00000000000..4b31cbc4499 --- /dev/null +++ b/packages/react-dev-utils/printHostingInstructions.js @@ -0,0 +1,114 @@ +/** + * 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. + */ + +'use strict'; + +const chalk = require('chalk'); +const url = require('url'); + +function printHostingInstructions( + appPackage, + publicUrl, + publicPath, + buildFolder, + useYarn +) { + const 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(publicPathname)}.` + ); + 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('build')} folder is ready to be deployed.`); + 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(); + if (useYarn) { + console.log(` ${chalk.cyan('yarn')} add --dev gh-pages`); + } else { + console.log(` ${chalk.cyan('npm')} install --save-dev gh-pages`); + } + console.log(); + console.log( + `Add the following script in your ${chalk.cyan('package.json')}.` + ); + console.log(); + console.log(` ${chalk.dim('// ...')}`); + console.log(` ${chalk.yellow('"scripts"')}: {`); + console.log(` ${chalk.dim('// ...')}`); + console.log( + ` ${chalk.yellow('"predeploy"')}: ${chalk.yellow('"npm run build",')}` + ); + console.log( + ` ${chalk.yellow('"deploy"')}: ${chalk.yellow('"gh-pages -d build"')}` + ); + console.log(' }'); + console.log(); + console.log('Then run:'); + } + console.log(); + console.log(` ${chalk.cyan(useYarn ? 'yarn' : 'npm')} run deploy`); + console.log(); + } 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(); + console.log(`The ${chalk.cyan('build')} folder is ready to be deployed.`); + console.log(); + } else { + if (publicUrl) { + // "homepage": "http://mywebsite.com" + 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')}.` + ); + console.log(); + } else { + // no homepage + 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')}.` + ); + 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(',')}` + ); + console.log(); + } + console.log( + `The ${chalk.cyan(buildFolder)} folder is ready to be deployed.` + ); + console.log('You may serve it with a static server:'); + console.log(); + if (useYarn) { + console.log(` ${chalk.cyan('yarn')} global add serve`); + } else { + console.log(` ${chalk.cyan('npm')} install -g serve`); + } + console.log(` ${chalk.cyan('serve')} -s build`); + console.log(); + } +} + +module.exports = printHostingInstructions; diff --git a/packages/react-dev-utils/webpackHotDevClient.js b/packages/react-dev-utils/webpackHotDevClient.js index 4e2d3cf1355..dd280dab5a9 100644 --- a/packages/react-dev-utils/webpackHotDevClient.js +++ b/packages/react-dev-utils/webpackHotDevClient.js @@ -26,8 +26,6 @@ var Entities = require('html-entities').AllHtmlEntities; var ansiHTML = require('./ansiHTML'); var entities = new Entities(); -var red = '#E36049'; - function createOverlayIframe(onIframeLoad) { var iframe = document.createElement('iframe'); iframe.id = 'react-dev-utils-webpack-hot-dev-client-overlay'; @@ -46,28 +44,53 @@ function createOverlayIframe(onIframeLoad) { } function addOverlayDivTo(iframe) { + // TODO: unify these styles with react-error-overlay + iframe.contentDocument.body.style.margin = 0; + iframe.contentDocument.body.style.maxWidth = '100vw'; + + var outerDiv = iframe.contentDocument.createElement('div'); + outerDiv.id = 'react-dev-utils-webpack-hot-dev-client-overlay-div'; + outerDiv.style.width = '100%'; + outerDiv.style.height = '100%'; + outerDiv.style.boxSizing = 'border-box'; + outerDiv.style.textAlign = 'center'; + outerDiv.style.backgroundColor = 'rgb(255, 255, 255)'; + var div = iframe.contentDocument.createElement('div'); - div.id = 'react-dev-utils-webpack-hot-dev-client-overlay-div'; - div.style.position = 'fixed'; + div.style.position = 'relative'; + div.style.display = 'inline-flex'; + div.style.flexDirection = 'column'; + div.style.height = '100%'; + div.style.width = '1024px'; + div.style.maxWidth = '100%'; + div.style.overflowX = 'hidden'; + div.style.overflowY = 'auto'; + div.style.padding = '0.5rem'; div.style.boxSizing = 'border-box'; - div.style.left = 0; - div.style.top = 0; - div.style.right = 0; - div.style.bottom = 0; - div.style.width = '100vw'; - div.style.height = '100vh'; - div.style.backgroundColor = '#fafafa'; - div.style.color = '#333'; - div.style.fontFamily = 'Menlo, Consolas, monospace'; - div.style.fontSize = 'large'; - div.style.padding = '2rem'; - div.style.lineHeight = '1.2'; + div.style.textAlign = 'start'; + div.style.fontFamily = 'Consolas, Menlo, monospace'; + div.style.fontSize = '11px'; div.style.whiteSpace = 'pre-wrap'; - div.style.overflow = 'auto'; - iframe.contentDocument.body.appendChild(div); + div.style.wordBreak = 'break-word'; + div.style.lineHeight = '1.5'; + div.style.color = 'rgb(41, 50, 56)'; + + outerDiv.appendChild(div); + iframe.contentDocument.body.appendChild(outerDiv); return div; } +function overlayHeaderStyle() { + return 'font-size: 2em;' + + 'font-family: sans-serif;' + + 'color: rgb(206, 17, 38);' + + 'white-space: pre-wrap;' + + 'margin: 0.75rem 2rem 0px 0px;' + + 'flex: 0 0 auto;' + + 'max-height: 35%;' + + 'overflow: auto;'; +} + var overlayIframe = null; var overlayDiv = null; var lastOnOverlayDivReady = null; @@ -103,11 +126,21 @@ function ensureOverlayDivExists(onOverlayDivReady) { function showErrorOverlay(message) { ensureOverlayDivExists(function onOverlayDivReady(overlayDiv) { - // Make it look similar to our terminal. - overlayDiv.innerHTML = 'Failed to compile.

' + - ansiHTML(entities.encode(message)); + // TODO: unify this with our runtime overlay + overlayDiv.innerHTML = '
Failed to compile


' + + '
' +
+      '' +
+      ansiHTML(entities.encode(message)) +
+      '
' + + '
' + + 'This error occurred during the build time and cannot be dismissed.
'; }); } diff --git a/packages/react-error-overlay/src/components/additional.js b/packages/react-error-overlay/src/components/additional.js index 4853c922c71..080f68f1dbb 100644 --- a/packages/react-error-overlay/src/components/additional.js +++ b/packages/react-error-overlay/src/components/additional.js @@ -16,16 +16,14 @@ function updateAdditional( additionalReference.removeChild(additionalReference.lastChild); } - let text = ' '; if (totalErrors <= 1) { - additionalReference.appendChild(document.createTextNode(text)); return; } - text = `Errors ${currentError} of ${totalErrors}`; + const span = document.createElement('span'); - span.appendChild(document.createTextNode(text)); const group = document.createElement('span'); applyStyles(group, groupStyle); + const left = document.createElement('button'); applyStyles(left, groupElemLeft); left.addEventListener('click', function(e: MouseEvent) { @@ -34,6 +32,7 @@ function updateAdditional( }); left.appendChild(document.createTextNode('←')); enableTabClick(left); + const right = document.createElement('button'); applyStyles(right, groupElemRight); right.addEventListener('click', function(e: MouseEvent) { @@ -42,9 +41,14 @@ function updateAdditional( }); right.appendChild(document.createTextNode('→')); enableTabClick(right); + group.appendChild(left); group.appendChild(right); span.appendChild(group); + + const text = `${currentError} of ${totalErrors} errors on the page`; + span.appendChild(document.createTextNode(text)); + additionalReference.appendChild(span); } diff --git a/packages/react-error-overlay/src/components/close.js b/packages/react-error-overlay/src/components/close.js index e868cc0d4ca..bba65131bd4 100644 --- a/packages/react-error-overlay/src/components/close.js +++ b/packages/react-error-overlay/src/components/close.js @@ -2,9 +2,10 @@ import { applyStyles } from '../utils/dom/css'; import { hintsStyle, hintStyle, closeButtonStyle } from '../styles'; -function createHint(document: Document, hint: string) { +function createHint(document: Document, hint: string, title: string) { const span = document.createElement('span'); span.appendChild(document.createTextNode(hint)); + span.setAttribute('title', title); applyStyles(span, hintStyle); return span; } @@ -14,7 +15,7 @@ function createClose(document: Document, callback: CloseCallback) { const hints = document.createElement('div'); applyStyles(hints, hintsStyle); - const close = createHint(document, '×'); + const close = createHint(document, '×', 'Click or press Escape to dismiss.'); close.addEventListener('click', () => callback()); applyStyles(close, closeButtonStyle); hints.appendChild(close); diff --git a/packages/react-error-overlay/src/components/footer.js b/packages/react-error-overlay/src/components/footer.js index 9ddfaf57c7e..bfedeef0d44 100644 --- a/packages/react-error-overlay/src/components/footer.js +++ b/packages/react-error-overlay/src/components/footer.js @@ -7,7 +7,7 @@ function createFooter(document: Document) { applyStyles(div, footerStyle); div.appendChild( document.createTextNode( - 'This screen is visible only in development. It will not appear when the app crashes in production.' + 'This screen is visible only in development. It will not appear if the app crashes in production.' ) ); div.appendChild(document.createElement('br')); diff --git a/packages/react-error-overlay/src/components/frame.js b/packages/react-error-overlay/src/components/frame.js index 70465a2eda0..a9ea0ea37da 100644 --- a/packages/react-error-overlay/src/components/frame.js +++ b/packages/react-error-overlay/src/components/frame.js @@ -120,6 +120,7 @@ function frameDiv( if (typeof onSourceClick === 'function') { let handler = onSourceClick; + enableTabClick(frameAnchor); frameAnchor.style.cursor = 'pointer'; frameAnchor.addEventListener('click', function() { handler(); diff --git a/packages/react-error-overlay/src/components/overlay.js b/packages/react-error-overlay/src/components/overlay.js index 8633524be13..1626fcdee57 100644 --- a/packages/react-error-overlay/src/components/overlay.js +++ b/packages/react-error-overlay/src/components/overlay.js @@ -1,6 +1,11 @@ /* @flow */ import { applyStyles } from '../utils/dom/css'; -import { overlayStyle, headerStyle, additionalStyle } from '../styles'; +import { + containerStyle, + overlayStyle, + headerStyle, + additionalStyle, +} from '../styles'; import { createClose } from './close'; import { createFrames } from './frames'; import { createFooter } from './footer'; @@ -28,24 +33,12 @@ function createOverlay( // Create overlay const overlay = document.createElement('div'); applyStyles(overlay, overlayStyle); - overlay.appendChild(createClose(document, closeCallback)); // Create container const container = document.createElement('div'); - container.className = 'cra-container'; + applyStyles(container, containerStyle); overlay.appendChild(container); - - // Create additional - const additional = document.createElement('div'); - applyStyles(additional, additionalStyle); - container.appendChild(additional); - updateAdditional( - document, - additional, - currentError, - totalErrors, - switchCallback - ); + container.appendChild(createClose(document, closeCallback)); // Create header const header = document.createElement('div'); @@ -71,6 +64,18 @@ function createOverlay( header.appendChild(document.createTextNode(finalMessage)); container.appendChild(header); + // Create "Errors X of Y" in case of multiple errors + const additional = document.createElement('div'); + applyStyles(additional, additionalStyle); + updateAdditional( + document, + additional, + currentError, + totalErrors, + switchCallback + ); + container.appendChild(additional); + // Create trace container.appendChild( createFrames(document, frames, frameSettings, contextSize, name) diff --git a/packages/react-error-overlay/src/overlay.js b/packages/react-error-overlay/src/overlay.js index fe29a6c7b36..cfcd1182911 100644 --- a/packages/react-error-overlay/src/overlay.js +++ b/packages/react-error-overlay/src/overlay.js @@ -35,7 +35,7 @@ import type { ErrorRecordReference } from './utils/errorRegister'; import type { StackFrame } from './utils/stack-frame'; import { iframeStyle } from './styles'; -import { injectCss, applyStyles } from './utils/dom/css'; +import { applyStyles } from './utils/dom/css'; import { createOverlay } from './components/overlay'; import { updateAdditional } from './components/additional'; @@ -45,33 +45,6 @@ let additionalReference = null; let errorReferences: ErrorRecordReference[] = []; let currReferenceIndex: number = -1; -const css = [ - '.cra-container {', - ' padding-right: 15px;', - ' padding-left: 15px;', - ' margin-right: auto;', - ' margin-left: auto;', - '}', - '', - '@media (min-width: 768px) {', - ' .cra-container {', - ' width: calc(750px - 6em);', - ' }', - '}', - '', - '@media (min-width: 992px) {', - ' .cra-container {', - ' width: calc(970px - 6em);', - ' }', - '}', - '', - '@media (min-width: 1200px) {', - ' .cra-container {', - ' width: calc(1170px - 6em);', - ' }', - '}', -].join('\n'); - function render(name: ?string, message: string, resolvedFrames: StackFrame[]) { disposeCurrentView(); @@ -105,9 +78,13 @@ function render(name: ?string, message: string, resolvedFrames: StackFrame[]) { keyEventHandler(type => shortcutHandler(type), event); }; } - injectCss(iframeReference.contentDocument, css); if (document.body != null) { - document.body.appendChild(overlay); + document.body.style.margin = '0'; + // Keep popup within body boundaries for iOS Safari + // $FlowFixMe + document.body.style['max-width'] = '100vw'; + + (document.body: any).appendChild(overlay); } additionalReference = additional; }; diff --git a/packages/react-error-overlay/src/styles.js b/packages/react-error-overlay/src/styles.js index 855778cbd13..8e6f0d07bc5 100644 --- a/packages/react-error-overlay/src/styles.js +++ b/packages/react-error-overlay/src/styles.js @@ -1,43 +1,50 @@ /* @flow */ const black = '#293238', darkGray = '#878e91', - lightGray = '#fafafa', red = '#ce1126', lightRed = '#fccfcf', - yellow = '#fbf5b4'; + yellow = '#fbf5b4', + white = '#ffffff'; const iframeStyle = { - 'background-color': lightGray, position: 'fixed', - top: '1em', - left: '1em', - bottom: '1em', - right: '1em', - width: 'calc(100% - 2em)', - height: 'calc(100% - 2em)', + top: '0', + left: '0', + width: '100%', + height: '100%', border: 'none', - 'border-radius': '3px', - 'box-shadow': '0 0 6px 0 rgba(0, 0, 0, 0.5)', 'z-index': 1337, }; const overlayStyle = { + width: '100%', + height: '100%', 'box-sizing': 'border-box', - padding: '4rem', + 'text-align': 'center', + 'background-color': white, +}; + +const containerStyle = { + position: 'relative', + display: 'inline-flex', + 'flex-direction': 'column', + height: '100%', + width: '1024px', + 'max-width': '100%', + 'overflow-x': 'hidden', + 'overflow-y': 'auto', + padding: '0.5rem', + 'box-sizing': 'border-box', + 'text-align': 'start', 'font-family': 'Consolas, Menlo, monospace', - color: black, + 'font-size': '11px', 'white-space': 'pre-wrap', - overflow: 'auto', - 'overflow-x': 'hidden', 'word-break': 'break-word', 'line-height': 1.5, + color: black, }; const hintsStyle = { - 'font-size': '0.8em', - 'margin-top': '-3em', - 'margin-bottom': '3em', - 'text-align': 'right', color: darkGray, }; @@ -47,34 +54,36 @@ const hintStyle = { }; const closeButtonStyle = { - 'font-size': '26px', color: black, - padding: '0.5em 1em', + 'line-height': '1rem', + 'font-size': '1.5rem', + padding: '1rem', cursor: 'pointer', position: 'absolute', right: 0, top: 0, }; -const additionalStyle = { - 'margin-bottom': '1.5em', - 'margin-top': '-4em', -}; +const additionalStyle = {}; const headerStyle = { - 'font-size': '1.7em', - 'font-weight': 'bold', + 'font-size': '2em', + 'font-family': 'sans-serif', color: red, 'white-space': 'pre-wrap', + margin: '0.75rem 2rem 0 0', // Prevent overlap with close button + flex: '0 0 auto', + 'max-height': '35%', + overflow: 'auto', }; const functionNameStyle = { 'margin-top': '1em', - 'font-size': '1.2em', }; const linkStyle = { 'font-size': '0.9em', + 'margin-bottom': '0.9em', }; const anchorStyle = { @@ -84,11 +93,12 @@ const anchorStyle = { const traceStyle = { 'font-size': '1em', + flex: '0 1 auto', + 'min-height': '0px', + overflow: 'auto', }; -const depStyle = { - 'font-size': '1.2em', -}; +const depStyle = {}; const primaryErrorStyle = { 'background-color': lightRed, @@ -100,19 +110,18 @@ const secondaryErrorStyle = { const omittedFramesStyle = { color: black, - 'font-size': '0.9em', - margin: '1.5em 0', cursor: 'pointer', }; const preStyle = { display: 'block', padding: '0.5em', - 'margin-top': '1.5em', - 'margin-bottom': '0px', + 'margin-top': '0.5em', + 'margin-bottom': '0.5em', 'overflow-x': 'auto', - 'font-size': '1.1em', - 'white-space': 'pre', + 'white-space': 'pre-wrap', + 'border-radius': '0.25rem', + 'background-color': 'rgba(206, 17, 38, .05)', }; const toggleStyle = { @@ -130,7 +139,7 @@ const hiddenStyle = { }; const groupStyle = { - 'margin-left': '1em', + 'margin-right': '1em', }; const _groupElemStyle = { @@ -152,15 +161,18 @@ const groupElemLeft = Object.assign({}, _groupElemStyle, { const groupElemRight = Object.assign({}, _groupElemStyle, { 'border-top-left-radius': '0px', 'border-bottom-left-radius': '0px', - 'margin-left': '-1px', + 'margin-right': '-1px', }); const footerStyle = { - 'text-align': 'center', + 'font-family': 'sans-serif', color: darkGray, + 'margin-top': '0.5rem', + flex: '0 0 auto', }; export { + containerStyle, iframeStyle, overlayStyle, hintsStyle, diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js index 98e57ea9e5b..540d54abf5c 100644 --- a/packages/react-scripts/config/webpack.config.dev.js +++ b/packages/react-scripts/config/webpack.config.dev.js @@ -18,6 +18,7 @@ const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin'); const eslintFormatter = require('react-dev-utils/eslintFormatter'); +const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin'); const getClientEnvironment = require('./env'); const paths = require('./paths'); @@ -106,6 +107,14 @@ module.exports = { // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/ 'react-native': 'react-native-web', }, + plugins: [ + // Prevents users from importing files from outside of src/ (or node_modules/). + // This often causes confusion because we only process files within src/ with babel. + // To fix this, we prevent you from importing files out of src/ -- if you'd like to, + // please link the files into your node_modules/ and let module-resolution kick in. + // Make sure your source files are compiled, as they will not be processed in any way. + new ModuleScopePlugin(paths.appSrc), + ], }, module: { strictExportPresence: true, @@ -137,10 +146,10 @@ module.exports = { include: paths.appSrc, }, // ** ADDING/UPDATING LOADERS ** - // The "url" loader handles all assets unless explicitly excluded. + // The "file" loader handles all assets unless explicitly excluded. // The `exclude` list *must* be updated with every change to loader extensions. // When adding a new loader, you must add its `test` - // as a new entry in the `exclude` list for "url" loader. + // as a new entry in the `exclude` list for "file" loader. // "file" loader makes sure those assets get served by WebpackDevServer. // When you `import` an asset, you get its (virtual) filename. @@ -224,7 +233,7 @@ module.exports = { ], }, // ** STOP ** Are you adding a new loader? - // Remember to add the new extension(s) to the "url" loader exclusion list. + // Remember to add the new extension(s) to the "file" loader exclusion list. ], }, plugins: [ @@ -252,6 +261,12 @@ module.exports = { // makes the discovery automatic so you don't have to restart. // See https://github.com/facebookincubator/create-react-app/issues/186 new WatchMissingNodeModulesPlugin(paths.appNodeModules), + // Moment.js is an extremely popular library that bundles large locale files + // by default due to how Webpack interprets its code. This is a practical + // solution that requires the user to opt into importing specific locales. + // https://github.com/jmblog/how-to-optimize-momentjs-with-webpack + // You can remove this if you don't use Moment.js: + new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), ], // Some libraries import Node modules but don't use them in the browser. // Tell Webpack to provide empty mocks for them so importing them works. diff --git a/packages/react-scripts/config/webpack.config.prod.js b/packages/react-scripts/config/webpack.config.prod.js index 7615894a755..ac672a4099a 100644 --- a/packages/react-scripts/config/webpack.config.prod.js +++ b/packages/react-scripts/config/webpack.config.prod.js @@ -17,7 +17,9 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); const ManifestPlugin = require('webpack-manifest-plugin'); const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); +const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin'); const eslintFormatter = require('react-dev-utils/eslintFormatter'); +const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin'); const paths = require('./paths'); const getClientEnvironment = require('./env'); @@ -103,6 +105,14 @@ module.exports = { // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/ 'react-native': 'react-native-web', }, + plugins: [ + // Prevents users from importing files from outside of src/ (or node_modules/). + // This often causes confusion because we only process files within src/ with babel. + // To fix this, we prevent you from importing files out of src/ -- if you'd like to, + // please link the files into your node_modules/ and let module-resolution kick in. + // Make sure your source files are compiled, as they will not be processed in any way. + new ModuleScopePlugin(paths.appSrc), + ], }, module: { strictExportPresence: true, @@ -136,10 +146,10 @@ module.exports = { include: paths.appSrc, }, // ** ADDING/UPDATING LOADERS ** - // The "url" loader handles all assets unless explicitly excluded. + // The "file" loader handles all assets unless explicitly excluded. // The `exclude` list *must* be updated with every change to loader extensions. // When adding a new loader, you must add its `test` - // as a new entry in the `exclude` list in the "url" loader. + // as a new entry in the `exclude` list in the "file" loader. // "file" loader makes sure those assets end up in the `build` folder. // When you `import` an asset, you get its filename. @@ -234,7 +244,7 @@ module.exports = { // Note: this won't work without `new ExtractTextPlugin()` in `plugins`. }, // ** STOP ** Are you adding a new loader? - // Remember to add the new extension(s) to the "url" loader exclusion list. + // Remember to add the new extension(s) to the "file" loader exclusion list. ], }, plugins: [ @@ -270,6 +280,10 @@ module.exports = { new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false, + // This feature has been reported as buggy a few times, such as: + // https://github.com/mishoo/UglifyJS2/issues/1964 + // We'll wait with enabling it by default until it is more solid. + reduce_vars: false, }, output: { comments: false, @@ -286,6 +300,32 @@ module.exports = { new ManifestPlugin({ fileName: 'asset-manifest.json', }), + // Generate a service worker script that will precache, and keep up to date, + // the HTML & assets that are part of the Webpack build. + new SWPrecacheWebpackPlugin({ + // By default, a cache-busting query parameter is appended to requests + // used to populate the caches, to ensure the responses are fresh. + // If a URL is already hashed by Webpack, then there is no concern + // about it being stale, and the cache-busting can be skipped. + dontCacheBustUrlsMatching: /\.\w{8}\./, + filename: 'service-worker.js', + logger(message) { + if (message.indexOf('Total precache size is') === 0) { + // This message occurs for every build and is a bit too noisy. + return; + } + console.log(message); + }, + minify: true, + navigateFallback: publicUrl + '/index.html', + staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/], + }), + // Moment.js is an extremely popular library that bundles large locale files + // by default due to how Webpack interprets its code. This is a practical + // solution that requires the user to opt into importing specific locales. + // https://github.com/jmblog/how-to-optimize-momentjs-with-webpack + // You can remove this if you don't use Moment.js: + new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), ], // Some libraries import Node modules but don't use them in the browser. // Tell Webpack to provide empty mocks for them so importing them works. diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index eaab334d4c3..cdc70febc60 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -26,7 +26,7 @@ "autoprefixer": "7.1.0", "babel-core": "6.24.1", "babel-eslint": "7.2.3", - "babel-jest": "20.0.1", + "babel-jest": "20.0.3", "babel-loader": "7.0.0", "babel-preset-react-app": "^3.0.0", "babel-runtime": "6.23.0", @@ -48,7 +48,7 @@ "fs-extra": "3.0.1", "html-webpack-plugin": "2.28.0", "inquirer": "3.0.6", - "jest": "20.0.1", + "jest": "20.0.3", "object-assign": "4.1.1", "postcss-flexbugs-fixes": "3.0.0", "postcss-loader": "2.0.5", @@ -56,6 +56,7 @@ "react-dev-utils": "^1.0.0", "react-error-overlay": "^1.0.0", "style-loader": "0.17.0", + "sw-precache-webpack-plugin": "0.9.1", "url-loader": "0.5.8", "webpack": "2.5.1", "webpack-dev-server": "2.4.5", diff --git a/packages/react-scripts/scripts/build.js b/packages/react-scripts/scripts/build.js index da9004f9ee6..8a4149370d2 100644 --- a/packages/react-scripts/scripts/build.js +++ b/packages/react-scripts/scripts/build.js @@ -23,14 +23,15 @@ process.on('unhandledRejection', err => { // Ensure environment variables are read. require('../config/env'); +const path = require('path'); const chalk = require('chalk'); const fs = require('fs-extra'); -const path = require('path'); -const url = require('url'); const webpack = require('webpack'); const config = require('../config/webpack.config.prod'); const paths = require('../config/paths'); const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); +const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); +const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); const measureFileSizesBeforeBuild = FileSizeReporter.measureFileSizesBeforeBuild; @@ -44,159 +45,74 @@ if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { // First, read the current file sizes in build directory. // This lets us display how much they changed later. -measureFileSizesBeforeBuild(paths.appBuild).then(previousFileSizes => { - // Remove all content but keep the directory so that - // if you're in it, you don't end up in Trash - fs.emptyDirSync(paths.appBuild); - - // Start the webpack build - build(previousFileSizes); - - // Merge with the public folder - copyPublicFolder(); -}); - -// Print out errors -function printErrors(summary, errors) { - console.log(chalk.red(summary)); - console.log(); - errors.forEach(err => { - console.log(err.message || err); - console.log(); - }); -} - -// Create the production build and print the deployment instructions. -function build(previousFileSizes) { - console.log('Creating an optimized production build...'); - - let compiler; - try { - compiler = webpack(config); - } catch (err) { - printErrors('Failed to compile.', [err]); - process.exit(1); - } - - compiler.run((err, stats) => { - if (err) { - printErrors('Failed to compile.', [err]); - process.exit(1); - } +measureFileSizesBeforeBuild(paths.appBuild) + .then(previousFileSizes => { + // Remove all content but keep the directory so that + // if you're in it, you don't end up in Trash + fs.emptyDirSync(paths.appBuild); + // Merge with the public folder + copyPublicFolder(); + // Start the webpack build + return build(previousFileSizes); + }) + .then( + ({ stats, previousFileSizes }) => { + console.log(chalk.green('Compiled successfully.')); + console.log(); - if (stats.compilation.errors.length) { - printErrors('Failed to compile.', stats.compilation.errors); - process.exit(1); - } + console.log('File sizes after gzip:'); + console.log(); + printFileSizesAfterBuild(stats, previousFileSizes); + console.log(); - if (process.env.CI && stats.compilation.warnings.length) { - printErrors( - 'Failed to compile. When process.env.CI = true, warnings are treated as failures. Most CI servers set this automatically.', - stats.compilation.warnings + const appPackage = require(paths.appPackageJson); + const publicUrl = paths.publicUrl; + const publicPath = config.output.publicPath; + const buildFolder = path.relative(process.cwd(), paths.appBuild); + printHostingInstructions( + appPackage, + publicUrl, + publicPath, + buildFolder, + useYarn ); + }, + err => { + console.log(chalk.red('Failed to compile.')); + console.log(); + console.log(err.message || err); + console.log(); process.exit(1); } + ); - console.log(chalk.green('Compiled successfully.')); - console.log(); - - console.log('File sizes after gzip:'); - console.log(); - printFileSizesAfterBuild(stats, previousFileSizes); - console.log(); +// Create the production build and print the deployment instructions. +function build(previousFileSizes) { + console.log('Creating an optimized production build...'); - const appPackage = require(paths.appPackageJson); - const publicUrl = paths.publicUrl; - const publicPath = config.output.publicPath; - const 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(publicPathname)}.` - ); - 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('build')} folder is ready to be deployed.`); - 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(); - if (useYarn) { - console.log(` ${chalk.cyan('yarn')} add --dev gh-pages`); - } else { - console.log(` ${chalk.cyan('npm')} install --save-dev gh-pages`); - } - console.log(); - console.log( - `Add the following script in your ${chalk.cyan('package.json')}.` - ); - console.log(); - console.log(` ${chalk.dim('// ...')}`); - console.log(` ${chalk.yellow('"scripts"')}: {`); - console.log(` ${chalk.dim('// ...')}`); - console.log( - ` ${chalk.yellow('"predeploy"')}: ${chalk.yellow('"npm run build",')}` - ); - console.log( - ` ${chalk.yellow('"deploy"')}: ${chalk.yellow('"gh-pages -d build"')}` - ); - console.log(' }'); - console.log(); - console.log('Then run:'); + let compiler = webpack(config); + return new Promise((resolve, reject) => { + compiler.run((err, stats) => { + if (err) { + return reject(err); } - console.log(); - console.log(` ${chalk.cyan(useYarn ? 'yarn' : 'npm')} run deploy`); - console.log(); - } 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(); - console.log(`The ${chalk.cyan('build')} folder is ready to be deployed.`); - console.log(); - } else { - if (publicUrl) { - // "homepage": "http://mywebsite.com" - 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')}.` - ); - console.log(); - } else { - // no homepage - 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')}.` - ); - console.log('For example, add this to build it for GitHub Pages:'); + const messages = formatWebpackMessages(stats.toJson({}, true)); + if (messages.errors.length) { + return reject(new Error(messages.errors.join('\n\n'))); + } + if (process.env.CI && messages.warnings.length) { console.log(); console.log( - ` ${chalk.green('"homepage"')} ${chalk.cyan(':')} ${chalk.green('"http://myname.github.io/myapp"')}${chalk.cyan(',')}` + chalk.yellow( + 'Treating warnings as errors because process.env.CI = true.\n' + + 'Most CI servers set it automatically.' + ) ); console.log(); + return reject(new Error(messages.warnings.join('\n\n'))); } - const build = path.relative(process.cwd(), paths.appBuild); - console.log(`The ${chalk.cyan(build)} folder is ready to be deployed.`); - console.log('You may serve it with a static server:'); - console.log(); - if (useYarn) { - console.log(` ${chalk.cyan('yarn')} global add serve`); - } else { - console.log(` ${chalk.cyan('npm')} install -g serve`); - } - console.log(` ${chalk.cyan('serve')} -s build`); - console.log(); - } + return resolve({ stats, previousFileSizes }); + }); }); } diff --git a/packages/react-scripts/template/README.md b/packages/react-scripts/template/README.md index c178e573816..50bbf7b694a 100644 --- a/packages/react-scripts/template/README.md +++ b/packages/react-scripts/template/README.md @@ -61,6 +61,10 @@ You can find the most recent version of this guide [here](https://github.com/fac - [Editor Integration](#editor-integration) - [Developing Components in Isolation](#developing-components-in-isolation) - [Making a Progressive Web App](#making-a-progressive-web-app) + - [Deployment](#deployment) - [Static Server](#static-server) - [Other Solutions](#other-solutions) @@ -81,6 +85,7 @@ You can find the most recent version of this guide [here](https://github.com/fac - [`npm test` hangs on macOS Sierra](#npm-test-hangs-on-macos-sierra) - [`npm run build` silently fails](#npm-run-build-silently-fails) - [`npm run build` fails on Heroku](#npm-run-build-fails-on-heroku) + - [Moment.js locales are missing](#momentjs-locales-are-missing) - [Something Missing?](#something-missing) ## Updating to New Releases @@ -1215,6 +1220,103 @@ Learn more about React Storybook: You can turn your React app into a [Progressive Web App](https://developers.google.com/web/progressive-web-apps/) by following the steps in [this repository](https://github.com/jeffposnick/create-react-pwa). + + ## Deployment `npm run build` creates a `build` directory with a production build of your app. Set up your favourite HTTP server so that a visitor to your site is served `index.html`, and requests to static paths like `/static/js/main..js` are served with the contents of the `/static/js/main..js` file. @@ -1290,6 +1392,19 @@ It will get copied to the `build` folder when you run `npm run build`. Now requests to `/todos/42` will be handled correctly both in development and in production. + + ### Building for Relative Paths By default, Create React App produces a build assuming your app is hosted at the server root.
@@ -1616,6 +1731,32 @@ It is reported that `npm run build` can fail on machines with no swap space, whi This may be a problem with case sensitive filenames. Please refer to [this section](#resolving-heroku-deployment-errors). +### Moment.js locales are missing + +If you use a [Moment.js](https://momentjs.com/), you might notice that only the English locale is available by default. This is because the locale files are large, and you probably only need a subset of [all the locales provided by Moment.js](https://momentjs.com/#multiple-locale-support). + +To add a specific Moment.js locale to your bundle, you need to import it explicitly.
+For example: + +```js +import moment from 'moment'; +import 'moment/locale/fr'; +``` + +If import multiple locales this way, you can later switch between them by calling `moment.locale()` with the locale name: + +```js +import moment from 'moment'; +import 'moment/locale/fr'; +import 'moment/locale/es'; + +// ... + +moment.locale('fr'); +``` + +This will only work for locales that have been explicitly imported before. + ## Something Missing? If you have ideas for more “How To” recipes that should be on this page, [let us know](https://github.com/facebookincubator/create-react-app/issues) or [contribute some!](https://github.com/facebookincubator/create-react-app/edit/master/packages/react-scripts/template/README.md) diff --git a/packages/react-scripts/template/public/index.html b/packages/react-scripts/template/public/index.html index 7f3e83f4e43..fc8689a2a29 100644 --- a/packages/react-scripts/template/public/index.html +++ b/packages/react-scripts/template/public/index.html @@ -3,9 +3,15 @@ + + +