diff --git a/docusaurus/docs/deployment.md b/docusaurus/docs/deployment.md index f4b257e6003..507628d9bc3 100644 --- a/docusaurus/docs/deployment.md +++ b/docusaurus/docs/deployment.md @@ -167,9 +167,9 @@ The AWS Amplify Console provides continuous deployment and hosting for modern we 1. Login to the Amplify Console [here](https://console.aws.amazon.com/amplify/home). 1. Connect your Create React App repo and pick a branch. If you're looking for a Create React App+Amplify starter, try the [create-react-app-auth-amplify starter](https://github.com/swaminator/create-react-app-auth-amplify) that demonstrates setting up auth in 10 minutes with Create React App. 1. The Amplify Console automatically detects the build settings. Choose Next. -1. Choose *Save and deploy*. +1. Choose _Save and deploy_. -If the build succeeds, the app is deployed and hosted on a global CDN with an amplifyapp.com domain. You can now continuously deploy changes to your frontend or backend. Continuous deployment allows developers to deploy updates to their frontend and backend on every code commit to their Git repository. +If the build succeeds, the app is deployed and hosted on a global CDN with an amplifyapp.com domain. You can now continuously deploy changes to your frontend or backend. Continuous deployment allows developers to deploy updates to their frontend and backend on every code commit to their Git repository. ## [Azure](https://azure.microsoft.com/) diff --git a/docusaurus/docs/getting-started.md b/docusaurus/docs/getting-started.md index ecc99198e03..ab52bc6c7b5 100644 --- a/docusaurus/docs/getting-started.md +++ b/docusaurus/docs/getting-started.md @@ -62,7 +62,6 @@ yarn create react-app my-app _`yarn create` is available in Yarn 0.25+_ - ### Creating a TypeScript app Follow our [Adding TypeScript](adding-typescript.md) documentation to create a TypeScript app. diff --git a/packages/react-scripts/config/modules.js b/packages/react-scripts/config/modules.js new file mode 100644 index 00000000000..b232fe37150 --- /dev/null +++ b/packages/react-scripts/config/modules.js @@ -0,0 +1,92 @@ +// @remove-on-eject-begin +/** + * 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. + */ +// @remove-on-eject-end +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const paths = require('./paths'); +const chalk = require('react-dev-utils/chalk'); + +/** + * Get the baseUrl of a compilerOptions object. + * + * @param {Object} options + */ +function getAdditionalModulePaths(options = {}) { + const baseUrl = options.baseUrl; + + // We need to explicitly check for null and undefined (and not a falsy value) because + // TypeScript treats an empty string as `.`. + if (baseUrl == null) { + // If there's no baseUrl set we respect NODE_PATH + // Note that NODE_PATH is deprecated and will be removed + // in the next major release of create-react-app. + + const nodePath = process.env.NODE_PATH || ''; + return nodePath.split(path.delimiter).filter(Boolean); + } + + const baseUrlResolved = path.resolve(paths.appPath, baseUrl); + + // We don't need to do anything if `baseUrl` is set to `node_modules`. This is + // the default behavior. + if (path.relative(paths.appNodeModules, baseUrlResolved) === '') { + return null; + } + + // Allow the user set the `baseUrl` to `appSrc`. + if (path.relative(paths.appSrc, baseUrlResolved) === '') { + return [paths.appSrc]; + } + + // Otherwise, throw an error. + throw new Error( + chalk.red.bold( + "Your project's `baseUrl` can only be set to `src` or `node_modules`." + + ' Create React App does not support other values at this time.' + ) + ); +} + +function getModules() { + // Check if TypeScript is setup + const hasTsConfig = fs.existsSync(paths.appTsConfig); + const hasJsConfig = fs.existsSync(paths.appJsConfig); + + if (hasTsConfig && hasJsConfig) { + throw new Error( + 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.' + ); + } + + let config; + + // If there's a tsconfig.json we assume it's a + // TypeScript project and set up the config + // based on tsconfig.json + if (hasTsConfig) { + config = require(paths.appTsConfig); + // Otherwise we'll check if there is jsconfig.json + // for non TS projects. + } else if (hasJsConfig) { + config = require(paths.appJsConfig); + } + + config = config || {}; + const options = config.compilerOptions || {}; + + const additionalModulePaths = getAdditionalModulePaths(options); + + return { + additionalModulePaths: additionalModulePaths, + hasTsConfig, + }; +} + +module.exports = getModules(); diff --git a/packages/react-scripts/config/paths.js b/packages/react-scripts/config/paths.js index b719054583b..e5a3e0b5374 100644 --- a/packages/react-scripts/config/paths.js +++ b/packages/react-scripts/config/paths.js @@ -84,6 +84,7 @@ module.exports = { appPackageJson: resolveApp('package.json'), appSrc: resolveApp('src'), appTsConfig: resolveApp('tsconfig.json'), + appJsConfig: resolveApp('jsconfig.json'), yarnLockFile: resolveApp('yarn.lock'), testsSetup: resolveModule(resolveApp, 'src/setupTests'), proxySetup: resolveApp('src/setupProxy.js'), @@ -106,6 +107,7 @@ module.exports = { appPackageJson: resolveApp('package.json'), appSrc: resolveApp('src'), appTsConfig: resolveApp('tsconfig.json'), + appJsConfig: resolveApp('jsconfig.json'), yarnLockFile: resolveApp('yarn.lock'), testsSetup: resolveModule(resolveApp, 'src/setupTests'), proxySetup: resolveApp('src/setupProxy.js'), @@ -140,6 +142,7 @@ if ( appPackageJson: resolveOwn('package.json'), appSrc: resolveOwn('template/src'), appTsConfig: resolveOwn('template/tsconfig.json'), + appJsConfig: resolveOwn('template/jsconfig.json'), yarnLockFile: resolveOwn('template/yarn.lock'), testsSetup: resolveModule(resolveOwn, 'template/src/setupTests'), proxySetup: resolveOwn('template/src/setupProxy.js'), diff --git a/packages/react-scripts/config/webpack.config.js b/packages/react-scripts/config/webpack.config.js index 42004dc7f6c..a0ba6c6306e 100644 --- a/packages/react-scripts/config/webpack.config.js +++ b/packages/react-scripts/config/webpack.config.js @@ -28,6 +28,7 @@ const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeM const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin'); const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent'); const paths = require('./paths'); +const modules = require('./modules'); const getClientEnvironment = require('./env'); const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin'); const ForkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin'); @@ -268,10 +269,7 @@ module.exports = function(webpackEnv) { // We placed these paths second because we want `node_modules` to "win" // if there are any conflicts. This matches Node resolution mechanism. // https://github.com/facebook/create-react-app/issues/253 - modules: ['node_modules', paths.appNodeModules].concat( - // It is guaranteed to exist because we tweak it in `env.js` - process.env.NODE_PATH.split(path.delimiter).filter(Boolean) - ), + modules: ['node_modules', paths.appNodeModules].concat(modules.additionalModulePaths || []), // These are the reasonable defaults supported by the Node ecosystem. // We also include JSX as a common component filename extension to support // some tools, although we do not recommend using it, see: diff --git a/packages/react-scripts/fixtures/kitchensink/integration/config.test.js b/packages/react-scripts/fixtures/kitchensink/integration/config.test.js new file mode 100644 index 00000000000..6d09b56c481 --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/integration/config.test.js @@ -0,0 +1,19 @@ +/** + * 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. + */ + +import initDOM from './initDOM'; + +describe('Integration', () => { + describe('jsconfig.json/tsconfig.json', () => { + it('Supports setting baseUrl to src', async () => { + const doc = await initDOM('base-url'); + + expect(doc.getElementById('feature-base-url').childElementCount).toBe(4); + doc.defaultView.close(); + }); + }); +}); diff --git a/packages/react-scripts/fixtures/kitchensink/integration/env.test.js b/packages/react-scripts/fixtures/kitchensink/integration/env.test.js index f86a09d52f6..7181c2fa78d 100644 --- a/packages/react-scripts/fixtures/kitchensink/integration/env.test.js +++ b/packages/react-scripts/fixtures/kitchensink/integration/env.test.js @@ -43,12 +43,6 @@ describe('Integration', () => { } }); - it('NODE_PATH', async () => { - doc = await initDOM('node-path'); - - expect(doc.getElementById('feature-node-path').childElementCount).toBe(4); - }); - it('PUBLIC_URL', async () => { doc = await initDOM('public-url'); diff --git a/packages/react-scripts/fixtures/kitchensink/jsconfig.json b/packages/react-scripts/fixtures/kitchensink/jsconfig.json new file mode 100644 index 00000000000..ec2332eb49c --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/jsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "baseUrl": "src" + } +} diff --git a/packages/react-scripts/fixtures/kitchensink/src/App.js b/packages/react-scripts/fixtures/kitchensink/src/App.js index 547138832f3..457b552601d 100644 --- a/packages/react-scripts/fixtures/kitchensink/src/App.js +++ b/packages/react-scripts/fixtures/kitchensink/src/App.js @@ -166,9 +166,6 @@ class App extends Component { this.setFeature(f.default) ); break; - case 'node-path': - import('./features/env/NodePath').then(f => this.setFeature(f.default)); - break; case 'no-ext-inclusion': import('./features/webpack/NoExtInclusion').then(f => this.setFeature(f.default) @@ -239,6 +236,11 @@ class App extends Component { this.setFeature(f.default) ); break; + case 'base-url': + import('./features/config/BaseUrl').then(f => + this.setFeature(f.default) + ); + break; default: this.setState({ error: `Missing feature "${feature}"` }); } diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/env/NodePath.js b/packages/react-scripts/fixtures/kitchensink/src/features/config/BaseUrl.js similarity index 95% rename from packages/react-scripts/fixtures/kitchensink/src/features/env/NodePath.js rename to packages/react-scripts/fixtures/kitchensink/src/features/config/BaseUrl.js index e89228e20b0..818d4db271b 100644 --- a/packages/react-scripts/fixtures/kitchensink/src/features/env/NodePath.js +++ b/packages/react-scripts/fixtures/kitchensink/src/features/config/BaseUrl.js @@ -30,7 +30,7 @@ export default class extends Component { render() { return ( - <div id="feature-node-path"> + <div id="feature-base-url"> {this.state.users.map(user => ( <div key={user.id}>{user.name}</div> ))} diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/env/NodePath.test.js b/packages/react-scripts/fixtures/kitchensink/src/features/config/BaseUrl.test.js similarity index 87% rename from packages/react-scripts/fixtures/kitchensink/src/features/env/NodePath.test.js rename to packages/react-scripts/fixtures/kitchensink/src/features/config/BaseUrl.test.js index 1de025d2f2f..aa8ddc396f7 100644 --- a/packages/react-scripts/fixtures/kitchensink/src/features/env/NodePath.test.js +++ b/packages/react-scripts/fixtures/kitchensink/src/features/config/BaseUrl.test.js @@ -7,9 +7,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import NodePath from './NodePath'; +import NodePath from './BaseUrl'; -describe('NODE_PATH', () => { +describe('BASE_URL', () => { it('renders without crashing', () => { const div = document.createElement('div'); return new Promise(resolve => { diff --git a/packages/react-scripts/scripts/build.js b/packages/react-scripts/scripts/build.js index ab9cdee8ba1..edbc6d11557 100644 --- a/packages/react-scripts/scripts/build.js +++ b/packages/react-scripts/scripts/build.js @@ -136,6 +136,18 @@ checkBrowsers(paths.appPath, isInteractive) // Create the production build and print the deployment instructions. function build(previousFileSizes) { + // We used to support resolving modules according to `NODE_PATH`. + // This now has been deprecated in favor of jsconfig/tsconfig.json + // This lets you use absolute paths in imports inside large monorepos: + if (process.env.NODE_PATH) { + console.log( + chalk.yellow( + 'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.' + ) + ); + console.log(); + } + console.log('Creating an optimized production build...'); const compiler = webpack(config); diff --git a/packages/react-scripts/scripts/start.js b/packages/react-scripts/scripts/start.js index 29d5033f251..d4726f5f67e 100644 --- a/packages/react-scripts/scripts/start.js +++ b/packages/react-scripts/scripts/start.js @@ -129,6 +129,19 @@ checkBrowsers(paths.appPath, isInteractive) if (isInteractive) { clearConsole(); } + + // We used to support resolving modules according to `NODE_PATH`. + // This now has been deprecated in favor of jsconfig/tsconfig.json + // This lets you use absolute paths in imports inside large monorepos: + if (process.env.NODE_PATH) { + console.log( + chalk.yellow( + 'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.' + ) + ); + console.log(); + } + console.log(chalk.cyan('Starting the development server...\n')); openBrowser(urls.localUrlForBrowser); }); diff --git a/packages/react-scripts/scripts/utils/createJestConfig.js b/packages/react-scripts/scripts/utils/createJestConfig.js index f57fc91b4d3..d2c788268b8 100644 --- a/packages/react-scripts/scripts/utils/createJestConfig.js +++ b/packages/react-scripts/scripts/utils/createJestConfig.js @@ -10,6 +10,7 @@ const fs = require('fs'); const chalk = require('react-dev-utils/chalk'); const paths = require('../../config/paths'); +const modules = require('../../config/modules'); module.exports = (resolve, rootDir, isEjecting) => { // Use this instead of `paths.testsSetup` to avoid putting @@ -49,6 +50,7 @@ module.exports = (resolve, rootDir, isEjecting) => { '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$', '^.+\\.module\\.(css|sass|scss)$', ], + modulePaths: modules.additionalModulePaths || [], moduleNameMapper: { '^react-native$': 'react-native-web', '^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy', diff --git a/packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js b/packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js index 4e6e8c96beb..7972c4e1012 100644 --- a/packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js +++ b/packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js @@ -18,7 +18,10 @@ const immer = require('react-dev-utils/immer').produce; const globby = require('react-dev-utils/globby').sync; function writeJson(fileName, object) { - fs.writeFileSync(fileName, JSON.stringify(object, null, 2).replace(/\n/g, os.EOL) + os.EOL); + fs.writeFileSync( + fileName, + JSON.stringify(object, null, 2).replace(/\n/g, os.EOL) + os.EOL + ); } function verifyNoTypeScript() { @@ -124,12 +127,6 @@ function verifyTypeScriptSetup() { value: 'preserve', reason: 'JSX is compiled by Babel', }, - // We do not support absolute imports, though this may come as a future - // enhancement - baseUrl: { - value: undefined, - reason: 'absolute imports are not supported (yet)', - }, paths: { value: undefined, reason: 'aliased imports are not supported' }, }; diff --git a/tasks/e2e-kitchensink.sh b/tasks/e2e-kitchensink.sh index d9810d67941..556712d3ec6 100755 --- a/tasks/e2e-kitchensink.sh +++ b/tasks/e2e-kitchensink.sh @@ -123,7 +123,6 @@ npm link "$temp_module_path/node_modules/test-integrity" # Test the build REACT_APP_SHELL_ENV_MESSAGE=fromtheshell \ - NODE_PATH=src \ PUBLIC_URL=http://www.example.org/spa/ \ yarn build @@ -135,7 +134,6 @@ exists build/static/js/main.*.js # https://facebook.github.io/jest/docs/en/troubleshooting.html#tests-are-extremely-slow-on-docker-and-or-continuous-integration-ci-server REACT_APP_SHELL_ENV_MESSAGE=fromtheshell \ CI=true \ - NODE_PATH=src \ NODE_ENV=test \ yarn test --no-cache --runInBand --testPathPattern=src diff --git a/tasks/local-test.sh b/tasks/local-test.sh index d1c01946c2f..ed4fcbe50ff 100755 --- a/tasks/local-test.sh +++ b/tasks/local-test.sh @@ -65,6 +65,9 @@ case ${test_suite} in "installs") test_command="./tasks/e2e-installs.sh" ;; + "behavior") + test_command="./tasks/e2e-behavior.sh" + ;; *) ;; esac diff --git a/test/fixtures/node_path/.disable-pnp b/test/fixtures/node_path/.disable-pnp new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/fixtures/node_path/.env b/test/fixtures/node_path/.env new file mode 100644 index 00000000000..f5fe60280fd --- /dev/null +++ b/test/fixtures/node_path/.env @@ -0,0 +1 @@ +NODE_PATH=src diff --git a/test/fixtures/node_path/index.test.js b/test/fixtures/node_path/index.test.js new file mode 100644 index 00000000000..eeba70a7420 --- /dev/null +++ b/test/fixtures/node_path/index.test.js @@ -0,0 +1,14 @@ +const testSetup = require('../__shared__/test-setup'); + +test('builds in development', async () => { + const { fulfilled } = await testSetup.scripts.start({ smoke: true }); + expect(fulfilled).toBe(true); +}); +test('builds in production', async () => { + const { fulfilled } = await testSetup.scripts.build(); + expect(fulfilled).toBe(true); +}); +test('passes tests', async () => { + const { fulfilled } = await testSetup.scripts.test(); + expect(fulfilled).toBe(true); +}); diff --git a/test/fixtures/node_path/package.json b/test/fixtures/node_path/package.json new file mode 100644 index 00000000000..fb6d17fe77d --- /dev/null +++ b/test/fixtures/node_path/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "prop-types": "^15.7.2", + "react": "latest", + "react-dom": "latest" + } +} diff --git a/test/fixtures/node_path/src/App.js b/test/fixtures/node_path/src/App.js new file mode 100644 index 00000000000..ba7136fc0aa --- /dev/null +++ b/test/fixtures/node_path/src/App.js @@ -0,0 +1,41 @@ +/** + * 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. + */ + +import React, { Component } from 'react'; + +import PropTypes from 'prop-types'; +import load from 'absoluteLoad'; + +export default class extends Component { + static propTypes = { + onReady: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + this.state = { users: [] }; + } + + async componentDidMount() { + const users = load(); + this.setState({ users }); + } + + componentDidUpdate() { + this.props.onReady(); + } + + render() { + return ( + <div> + {this.state.users.map(user => ( + <div key={user.id}>{user.name}</div> + ))} + </div> + ); + } +} diff --git a/test/fixtures/node_path/src/App.test.js b/test/fixtures/node_path/src/App.test.js new file mode 100644 index 00000000000..bacc39efcfe --- /dev/null +++ b/test/fixtures/node_path/src/App.test.js @@ -0,0 +1,17 @@ +/** + * 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. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; + +test('loads modules absolutely with NODE_PATH', () => { + const div = document.createElement('div'); + return new Promise(resolve => { + ReactDOM.render(<App onReady={resolve} />, div); + }); +}); diff --git a/test/fixtures/node_path/src/absoluteLoad.js b/test/fixtures/node_path/src/absoluteLoad.js new file mode 100644 index 00000000000..5c4f7842e28 --- /dev/null +++ b/test/fixtures/node_path/src/absoluteLoad.js @@ -0,0 +1,13 @@ +/** + * 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. + */ + +export default () => [ + { id: 1, name: '1' }, + { id: 2, name: '2' }, + { id: 3, name: '3' }, + { id: 4, name: '4' }, +]; diff --git a/test/fixtures/node_path/src/index.js b/test/fixtures/node_path/src/index.js new file mode 100644 index 00000000000..b597a44232c --- /dev/null +++ b/test/fixtures/node_path/src/index.js @@ -0,0 +1,5 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; + +ReactDOM.render(<App />, document.getElementById('root')); diff --git a/test/fixtures/typescript/src/App.test.ts b/test/fixtures/typescript/src/App.test.ts index f02c462b553..636c014031b 100644 --- a/test/fixtures/typescript/src/App.test.ts +++ b/test/fixtures/typescript/src/App.test.ts @@ -13,3 +13,13 @@ it('supports decorators', () => { const app = new App(); expect(app.decorated).toBe(42); }); + +it('supports loading modules with baseUrl', () => { + const app = new App(); + expect(app.users).toEqual([ + { id: 1, name: '1' }, + { id: 2, name: '2' }, + { id: 3, name: '3' }, + { id: 4, name: '4' }, + ]); +}); diff --git a/test/fixtures/typescript/src/App.ts b/test/fixtures/typescript/src/App.ts index d803c92d199..665f8b49f1f 100644 --- a/test/fixtures/typescript/src/App.ts +++ b/test/fixtures/typescript/src/App.ts @@ -1,3 +1,5 @@ +import absoluteLoad from 'absoluteLoad'; + interface MyType { foo: number; bar: boolean; @@ -12,6 +14,7 @@ class App { n = App.foo.baz!.n; @propertyDecorator decorated = 5; + users = absoluteLoad(); } function annotation(target: any) { diff --git a/test/fixtures/typescript/src/absoluteLoad.ts b/test/fixtures/typescript/src/absoluteLoad.ts new file mode 100644 index 00000000000..5c4f7842e28 --- /dev/null +++ b/test/fixtures/typescript/src/absoluteLoad.ts @@ -0,0 +1,13 @@ +/** + * 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. + */ + +export default () => [ + { id: 1, name: '1' }, + { id: 2, name: '2' }, + { id: 3, name: '3' }, + { id: 4, name: '4' }, +]; diff --git a/test/fixtures/typescript/tsconfig.json b/test/fixtures/typescript/tsconfig.json index 504cd646e14..7bda36e19bf 100644 --- a/test/fixtures/typescript/tsconfig.json +++ b/test/fixtures/typescript/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "baseUrl": "src", "experimentalDecorators": true } }