diff --git a/.travis.yml b/.travis.yml index 860000b5c9b..f27e0e9a2b8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,11 +15,13 @@ script: - 'if [ $TEST_SUITE = "installs" ]; then tasks/e2e-installs.sh; fi' - 'if [ $TEST_SUITE = "kitchensink" ]; then tasks/e2e-kitchensink.sh; fi' - 'if [ $TEST_SUITE = "old-node" ]; then tasks/e2e-old-node.sh; fi' + - 'if [ $TEST_SUITE = "monorepos" ]; then tasks/e2e-monorepos.sh; fi' env: matrix: - TEST_SUITE=simple - TEST_SUITE=installs - TEST_SUITE=kitchensink + - TEST_SUITE=monorepos matrix: include: - node_js: 0.10 diff --git a/appveyor.yml b/appveyor.yml index 5d957aa7eea..db1d640caa1 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -8,13 +8,16 @@ environment: test_suite: "installs" - nodejs_version: 8 test_suite: "kitchensink" + - nodejs_version: 8 + test_suite: "monorepos" - nodejs_version: 6 test_suite: "simple" - nodejs_version: 6 test_suite: "installs" - nodejs_version: 6 test_suite: "kitchensink" - + - nodejs_version: 6 + test_suite: "monorepos" cache: - node_modules -> appveyor.cleanup-cache.txt - packages\react-scripts\node_modules -> appveyor.cleanup-cache.txt diff --git a/packages/react-scripts/config/jest/babelTransform.js b/packages/react-scripts/config/jest/babelTransform.js index 02742e90c6c..8792f3f6473 100644 --- a/packages/react-scripts/config/jest/babelTransform.js +++ b/packages/react-scripts/config/jest/babelTransform.js @@ -1,15 +1,18 @@ -// @remove-file-on-eject +// @remove-on-eject-begin /** * Copyright (c) 2014-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 babelJest = require('babel-jest'); module.exports = babelJest.createTransformer({ presets: [require.resolve('babel-preset-react-app')], + // @remove-on-eject-begin babelrc: false, + // @remove-on-eject-end }); diff --git a/packages/react-scripts/config/paths.js b/packages/react-scripts/config/paths.js index b9dd95051f2..9c74ceaf177 100644 --- a/packages/react-scripts/config/paths.js +++ b/packages/react-scripts/config/paths.js @@ -11,6 +11,8 @@ const path = require('path'); const fs = require('fs'); const url = require('url'); +const findPkg = require('find-pkg'); +const globby = require('globby'); // Make sure any symlinks in the project folder are resolved: // https://github.com/facebookincubator/create-react-app/issues/637 @@ -63,6 +65,8 @@ module.exports = { servedPath: getServedPath(resolveApp('package.json')), }; +let checkForMonorepo = true; + // @remove-on-eject-begin const resolveOwn = relativePath => path.resolve(__dirname, '..', relativePath); @@ -86,17 +90,13 @@ module.exports = { ownNodeModules: resolveOwn('node_modules'), // This is empty on npm 3 }; -const ownPackageJson = require('../package.json'); -const reactScriptsPath = resolveApp(`node_modules/${ownPackageJson.name}`); -const reactScriptsLinked = - fs.existsSync(reactScriptsPath) && - fs.lstatSync(reactScriptsPath).isSymbolicLink(); - -// config before publish: we're in ./packages/react-scripts/config/ -if ( - !reactScriptsLinked && - __dirname.indexOf(path.join('packages', 'react-scripts', 'config')) !== -1 -) { +// detect if template should be used, ie. when cwd is react-scripts itself +const useTemplate = + appDirectory === fs.realpathSync(path.join(__dirname, '..')); + +checkForMonorepo = !useTemplate; + +if (useTemplate) { module.exports = { dotenv: resolveOwn('template/.env'), appPath: resolveApp('.'), @@ -117,3 +117,40 @@ if ( }; } // @remove-on-eject-end + +module.exports.srcPaths = [module.exports.appSrc]; + +const findPkgs = (rootPath, globPatterns) => { + const globOpts = { + cwd: rootPath, + strict: true, + absolute: true, + }; + return globPatterns + .reduce( + (pkgs, pattern) => + pkgs.concat(globby.sync(path.join(pattern, 'package.json'), globOpts)), + [] + ) + .map(f => path.dirname(path.normalize(f))); +}; + +const getMonorepoPkgPaths = () => { + const monoPkgPath = findPkg.sync(path.resolve(appDirectory, '..')); + if (monoPkgPath) { + // get monorepo config from yarn workspace + const pkgPatterns = require(monoPkgPath).workspaces; + const pkgPaths = findPkgs(path.dirname(monoPkgPath), pkgPatterns); + // only include monorepo pkgs if app itself is included in monorepo + if (pkgPaths.indexOf(appDirectory) !== -1) { + return pkgPaths.filter(f => fs.realpathSync(f) !== appDirectory); + } + } + return []; +}; + +if (checkForMonorepo) { + // if app is in a monorepo (lerna or yarn workspace), treat other packages in + // the monorepo as if they are app source + Array.prototype.push.apply(module.exports.srcPaths, getMonorepoPkgPaths()); +} diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js index 0d7db7c84a2..ce19de97420 100644 --- a/packages/react-scripts/config/webpack.config.dev.js +++ b/packages/react-scripts/config/webpack.config.dev.js @@ -145,10 +145,10 @@ module.exports = { options: { formatter: eslintFormatter, eslintPath: require.resolve('eslint'), - // @remove-on-eject-begin baseConfig: { extends: [require.resolve('eslint-config-react-app')], }, + // @remove-on-eject-begin ignore: false, useEslintrc: false, // @remove-on-eject-end @@ -156,7 +156,8 @@ module.exports = { loader: require.resolve('eslint-loader'), }, ], - include: paths.appSrc, + include: paths.srcPaths, + exclude: [/[/\\\\]node_modules[/\\\\]/], }, { // "oneOf" will traverse all following loaders until one will @@ -178,7 +179,8 @@ module.exports = { // The preset includes JSX, Flow, and some ESnext features. { test: /\.(js|jsx|mjs)$/, - include: paths.appSrc, + include: paths.srcPaths, + exclude: [/[/\\\\]node_modules[/\\\\]/], use: [ // This loader parallelizes code compilation, it is optional but // improves compile time on larger projects @@ -188,8 +190,8 @@ module.exports = { options: { // @remove-on-eject-begin babelrc: false, - presets: [require.resolve('babel-preset-react-app')], // @remove-on-eject-end + presets: [require.resolve('babel-preset-react-app')], // This is a feature of `babel-loader` for webpack (not Babel itself). // It enables caching results in ./node_modules/.cache/babel-loader/ // directory for faster rebuilds. @@ -275,8 +277,8 @@ module.exports = { options: { // @remove-on-eject-begin babelrc: false, - presets: [require.resolve('babel-preset-react-app')], // @remove-on-eject-end + presets: [require.resolve('babel-preset-react-app')], cacheDirectory: true, }, }, diff --git a/packages/react-scripts/config/webpack.config.prod.js b/packages/react-scripts/config/webpack.config.prod.js index 98ddb463391..0926c577ff3 100644 --- a/packages/react-scripts/config/webpack.config.prod.js +++ b/packages/react-scripts/config/webpack.config.prod.js @@ -152,12 +152,12 @@ module.exports = { options: { formatter: eslintFormatter, eslintPath: require.resolve('eslint'), - // @remove-on-eject-begin // TODO: consider separate config for production, // e.g. to enable no-console and no-debugger only in production. baseConfig: { extends: [require.resolve('eslint-config-react-app')], }, + // @remove-on-eject-begin ignore: false, useEslintrc: false, // @remove-on-eject-end @@ -165,7 +165,8 @@ module.exports = { loader: require.resolve('eslint-loader'), }, ], - include: paths.appSrc, + include: paths.srcPaths, + exclude: [/[/\\\\]node_modules[/\\\\]/], }, { // "oneOf" will traverse all following loaders until one will @@ -186,7 +187,8 @@ module.exports = { // The preset includes JSX, Flow, and some ESnext features. { test: /\.(js|jsx|mjs)$/, - include: paths.appSrc, + include: paths.srcPaths, + exclude: [/[/\\\\]node_modules[/\\\\]/], use: [ // This loader parallelizes code compilation, it is optional but // improves compile time on larger projects @@ -196,8 +198,8 @@ module.exports = { options: { // @remove-on-eject-begin babelrc: false, - presets: [require.resolve('babel-preset-react-app')], // @remove-on-eject-end + presets: [require.resolve('babel-preset-react-app')], compact: true, highlightCode: true, }, @@ -317,8 +319,8 @@ module.exports = { options: { // @remove-on-eject-begin babelrc: false, - presets: [require.resolve('babel-preset-react-app')], // @remove-on-eject-end + presets: [require.resolve('babel-preset-react-app')], cacheDirectory: true, }, }, diff --git a/packages/react-scripts/fixtures/kitchensink/.babelrc b/packages/react-scripts/fixtures/kitchensink/.babelrc new file mode 100644 index 00000000000..c14b2828d16 --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["react-app"] +} diff --git a/packages/react-scripts/fixtures/monorepos/packages/comp1/index.js b/packages/react-scripts/fixtures/monorepos/packages/comp1/index.js new file mode 100644 index 00000000000..f3cb7964952 --- /dev/null +++ b/packages/react-scripts/fixtures/monorepos/packages/comp1/index.js @@ -0,0 +1,5 @@ +import React from 'react'; + +const Comp1 = () =>
Comp1
; + +export default Comp1; diff --git a/packages/react-scripts/fixtures/monorepos/packages/comp1/index.test.js b/packages/react-scripts/fixtures/monorepos/packages/comp1/index.test.js new file mode 100644 index 00000000000..25160484031 --- /dev/null +++ b/packages/react-scripts/fixtures/monorepos/packages/comp1/index.test.js @@ -0,0 +1,8 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import Comp1 from '.'; + +it('renders Comp1 without crashing', () => { + const div = document.createElement('div'); + ReactDOM.render(, div); +}); diff --git a/packages/react-scripts/fixtures/monorepos/packages/comp1/package.json b/packages/react-scripts/fixtures/monorepos/packages/comp1/package.json new file mode 100644 index 00000000000..baa4c31dc71 --- /dev/null +++ b/packages/react-scripts/fixtures/monorepos/packages/comp1/package.json @@ -0,0 +1,10 @@ +{ + "name": "comp1", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "devDependencies": { + "react": "^16.2.0", + "react-dom": "^16.2.0" + } +} diff --git a/packages/react-scripts/fixtures/monorepos/packages/comp2/index.js b/packages/react-scripts/fixtures/monorepos/packages/comp2/index.js new file mode 100644 index 00000000000..6dcc69dacf1 --- /dev/null +++ b/packages/react-scripts/fixtures/monorepos/packages/comp2/index.js @@ -0,0 +1,11 @@ +import React from 'react'; + +import Comp1 from 'comp1'; + +const Comp2 = () => ( +
+ Comp2, nested Comp1: +
+); + +export default Comp2; diff --git a/packages/react-scripts/fixtures/monorepos/packages/comp2/index.test.js b/packages/react-scripts/fixtures/monorepos/packages/comp2/index.test.js new file mode 100644 index 00000000000..6d022e87d54 --- /dev/null +++ b/packages/react-scripts/fixtures/monorepos/packages/comp2/index.test.js @@ -0,0 +1,8 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import Comp2 from '.'; + +it('renders Comp2 without crashing', () => { + const div = document.createElement('div'); + ReactDOM.render(, div); +}); diff --git a/packages/react-scripts/fixtures/monorepos/packages/comp2/package.json b/packages/react-scripts/fixtures/monorepos/packages/comp2/package.json new file mode 100644 index 00000000000..329b14077ba --- /dev/null +++ b/packages/react-scripts/fixtures/monorepos/packages/comp2/package.json @@ -0,0 +1,13 @@ +{ + "name": "comp2", + "dependencies": { + "comp1": "^1.0.0" + }, + "devDependencies": { + "react": "^16.2.0", + "react-dom": "^16.2.0" + }, + "version": "1.0.0", + "main": "index.js", + "license": "MIT" +} diff --git a/packages/react-scripts/fixtures/monorepos/packages/cra-app1/.gitignore b/packages/react-scripts/fixtures/monorepos/packages/cra-app1/.gitignore new file mode 100644 index 00000000000..d30f40ef442 --- /dev/null +++ b/packages/react-scripts/fixtures/monorepos/packages/cra-app1/.gitignore @@ -0,0 +1,21 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +/node_modules + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/packages/react-scripts/fixtures/monorepos/packages/cra-app1/package.json b/packages/react-scripts/fixtures/monorepos/packages/cra-app1/package.json new file mode 100644 index 00000000000..218edb79d6b --- /dev/null +++ b/packages/react-scripts/fixtures/monorepos/packages/cra-app1/package.json @@ -0,0 +1,32 @@ +{ + "name": "cra-app1", + "version": "0.1.0", + "private": true, + "dependencies": { + "comp2": "^1.0.0", + "react": "^16.2.0", + "react-dom": "^16.2.0" + }, + "devDependencies": { + "react-scripts": "latest" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + }, + "browserslist": { + "development": [ + "last 2 chrome versions", + "last 2 firefox versions", + "last 2 edge versions" + ], + "production": [ + ">1%", + "last 4 versions", + "Firefox ESR", + "not ie < 11" + ] + } +} diff --git a/packages/react-scripts/fixtures/monorepos/packages/cra-app1/public/favicon.ico b/packages/react-scripts/fixtures/monorepos/packages/cra-app1/public/favicon.ico new file mode 100644 index 00000000000..a11777cc471 Binary files /dev/null and b/packages/react-scripts/fixtures/monorepos/packages/cra-app1/public/favicon.ico differ diff --git a/packages/react-scripts/fixtures/monorepos/packages/cra-app1/public/index.html b/packages/react-scripts/fixtures/monorepos/packages/cra-app1/public/index.html new file mode 100644 index 00000000000..ed0ebafa1b7 --- /dev/null +++ b/packages/react-scripts/fixtures/monorepos/packages/cra-app1/public/index.html @@ -0,0 +1,40 @@ + + + + + + + + + + + React App + + + +
+ + + diff --git a/packages/react-scripts/fixtures/monorepos/packages/cra-app1/public/manifest.json b/packages/react-scripts/fixtures/monorepos/packages/cra-app1/public/manifest.json new file mode 100644 index 00000000000..ef19ec243e7 --- /dev/null +++ b/packages/react-scripts/fixtures/monorepos/packages/cra-app1/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": "./index.html", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/packages/react-scripts/fixtures/monorepos/packages/cra-app1/src/App.css b/packages/react-scripts/fixtures/monorepos/packages/cra-app1/src/App.css new file mode 100644 index 00000000000..c5c6e8a68ad --- /dev/null +++ b/packages/react-scripts/fixtures/monorepos/packages/cra-app1/src/App.css @@ -0,0 +1,28 @@ +.App { + text-align: center; +} + +.App-logo { + animation: App-logo-spin infinite 20s linear; + height: 80px; +} + +.App-header { + background-color: #222; + height: 150px; + padding: 20px; + color: white; +} + +.App-title { + font-size: 1.5em; +} + +.App-intro { + font-size: large; +} + +@keyframes App-logo-spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} diff --git a/packages/react-scripts/fixtures/monorepos/packages/cra-app1/src/App.js b/packages/react-scripts/fixtures/monorepos/packages/cra-app1/src/App.js new file mode 100644 index 00000000000..ab9f3f95793 --- /dev/null +++ b/packages/react-scripts/fixtures/monorepos/packages/cra-app1/src/App.js @@ -0,0 +1,24 @@ +import React, { Component } from 'react'; +import logo from './logo.svg'; +import './App.css'; + +import Comp2 from 'comp2'; + +class App extends Component { + render() { + return ( +
+
+ logo +

YarnWS-CraApp1

+
+

+ To get started, edit src/App.js and save to reload. +

+ +
+ ); + } +} + +export default App; diff --git a/packages/react-scripts/fixtures/monorepos/packages/cra-app1/src/App.test.js b/packages/react-scripts/fixtures/monorepos/packages/cra-app1/src/App.test.js new file mode 100644 index 00000000000..a754b201bf9 --- /dev/null +++ b/packages/react-scripts/fixtures/monorepos/packages/cra-app1/src/App.test.js @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; + +it('renders without crashing', () => { + const div = document.createElement('div'); + ReactDOM.render(, div); + ReactDOM.unmountComponentAtNode(div); +}); diff --git a/packages/react-scripts/fixtures/monorepos/packages/cra-app1/src/index.css b/packages/react-scripts/fixtures/monorepos/packages/cra-app1/src/index.css new file mode 100644 index 00000000000..b4cc7250b98 --- /dev/null +++ b/packages/react-scripts/fixtures/monorepos/packages/cra-app1/src/index.css @@ -0,0 +1,5 @@ +body { + margin: 0; + padding: 0; + font-family: sans-serif; +} diff --git a/packages/react-scripts/fixtures/monorepos/packages/cra-app1/src/index.js b/packages/react-scripts/fixtures/monorepos/packages/cra-app1/src/index.js new file mode 100644 index 00000000000..395b74997b2 --- /dev/null +++ b/packages/react-scripts/fixtures/monorepos/packages/cra-app1/src/index.js @@ -0,0 +1,6 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import './index.css'; +import App from './App'; + +ReactDOM.render(, document.getElementById('root')); diff --git a/packages/react-scripts/fixtures/monorepos/packages/cra-app1/src/logo.svg b/packages/react-scripts/fixtures/monorepos/packages/cra-app1/src/logo.svg new file mode 100644 index 00000000000..6b60c1042f5 --- /dev/null +++ b/packages/react-scripts/fixtures/monorepos/packages/cra-app1/src/logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/react-scripts/fixtures/monorepos/yarn-ws/package.json b/packages/react-scripts/fixtures/monorepos/yarn-ws/package.json new file mode 100644 index 00000000000..aad9ea832aa --- /dev/null +++ b/packages/react-scripts/fixtures/monorepos/yarn-ws/package.json @@ -0,0 +1,4 @@ +{ + "private": true, + "workspaces": ["packages/*"] +} diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index 3c670a23dd6..ea668999b41 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -43,7 +43,9 @@ "eslint-plugin-react": "7.5.1", "extract-text-webpack-plugin": "3.0.2", "file-loader": "1.1.6", + "find-pkg": "1.0.0", "fs-extra": "5.0.0", + "globby": "7.1.1", "html-webpack-plugin": "2.30.1", "identity-obj-proxy": "3.0.0", "jest": "22.1.2", diff --git a/packages/react-scripts/scripts/eject.js b/packages/react-scripts/scripts/eject.js index f3826abb4eb..76f8be64be2 100644 --- a/packages/react-scripts/scripts/eject.js +++ b/packages/react-scripts/scripts/eject.js @@ -109,7 +109,7 @@ inquirer const jestConfig = createJestConfig( filePath => path.posix.join('', filePath), null, - true + paths.srcPaths ); console.log(); @@ -205,18 +205,6 @@ inquirer console.log(` Adding ${cyan('Jest')} configuration`); appPackage.jest = jestConfig; - // Add Babel config - console.log(` Adding ${cyan('Babel')} preset`); - appPackage.babel = { - presets: ['react-app'], - }; - - // Add ESlint config - console.log(` Adding ${cyan('ESLint')} configuration`); - appPackage.eslintConfig = { - extends: 'react-app', - }; - fs.writeFileSync( path.join(appPath, 'package.json'), JSON.stringify(appPackage, null, 2) + os.EOL diff --git a/packages/react-scripts/scripts/test.js b/packages/react-scripts/scripts/test.js index 470e8e35721..7d2acf77ea2 100644 --- a/packages/react-scripts/scripts/test.js +++ b/packages/react-scripts/scripts/test.js @@ -53,7 +53,7 @@ argv.push( createJestConfig( relativePath => path.resolve(__dirname, '..', relativePath), path.resolve(paths.appSrc, '..'), - false + paths.srcPaths ) ) ); diff --git a/packages/react-scripts/scripts/utils/createJestConfig.js b/packages/react-scripts/scripts/utils/createJestConfig.js index cdea70699c0..ea92a4f21ec 100644 --- a/packages/react-scripts/scripts/utils/createJestConfig.js +++ b/packages/react-scripts/scripts/utils/createJestConfig.js @@ -8,16 +8,19 @@ 'use strict'; const fs = require('fs'); +const path = require('path'); const chalk = require('chalk'); const paths = require('../../config/paths'); -module.exports = (resolve, rootDir, isEjecting) => { +module.exports = (resolve, rootDir, srcRoots) => { // Use this instead of `paths.testsSetup` to avoid putting // an absolute filename into configuration after ejecting. const setupTestsFile = fs.existsSync(paths.testsSetup) ? '/src/setupTests.js' : undefined; + const toRelRootDir = f => '/' + path.relative(rootDir || '', f); + // TODO: I don't know if it's safe or not to just use / as path separator // in Jest configs. We need help from somebody with Windows to determine this. const config = { @@ -25,15 +28,15 @@ module.exports = (resolve, rootDir, isEjecting) => { setupFiles: [resolve('config/polyfills.js')], setupTestFrameworkScriptFile: setupTestsFile, testMatch: [ - '/src/**/__tests__/**/*.{js,jsx,mjs}', - '/src/**/?(*.)(spec|test).{js,jsx,mjs}', + '**/__tests__/**/*.{js,jsx,mjs}', + '**/?(*.)(spec|test).{js,jsx,mjs}', ], + // where to search for files/tests + roots: srcRoots.map(toRelRootDir), testEnvironment: 'node', testURL: 'http://localhost', transform: { - '^.+\\.(js|jsx|mjs)$': isEjecting - ? '/node_modules/babel-jest' - : resolve('config/jest/babelTransform.js'), + '^.+\\.(js|jsx|mjs)$': resolve('config/jest/babelTransform.js'), '^.+\\.css$': resolve('config/jest/cssTransform.js'), '^(?!.*\\.(js|jsx|mjs|css|json)$)': resolve( 'config/jest/fileTransform.js' @@ -60,6 +63,7 @@ module.exports = (resolve, rootDir, isEjecting) => { if (rootDir) { config.rootDir = rootDir; } + const overrides = Object.assign({}, require(paths.appPackageJson).jest); const supportedKeys = [ 'collectCoverageFrom', diff --git a/packages/react-scripts/template/README.md b/packages/react-scripts/template/README.md index 04dc4d9b971..cb43dd0ad52 100644 --- a/packages/react-scripts/template/README.md +++ b/packages/react-scripts/template/README.md @@ -73,6 +73,7 @@ You can find the most recent version of this guide [here](https://github.com/fac - [Developing Components in Isolation](#developing-components-in-isolation) - [Getting Started with Storybook](#getting-started-with-storybook) - [Getting Started with Styleguidist](#getting-started-with-styleguidist) +- [Sharing Components in a Monorepo](#sharing-components-in-a-monorepo) - [Publishing Components to npm](#publishing-components-to-npm) - [Making a Progressive Web App](#making-a-progressive-web-app) - [Opting Out of Caching](#opting-out-of-caching) @@ -1819,6 +1820,66 @@ Learn more about React Styleguidist: * [GitHub Repo](https://github.com/styleguidist/react-styleguidist) * [Documentation](https://react-styleguidist.js.org/docs/getting-started.html) +## Sharing Components in a Monorepo +A typical monorepo folder structure looks like this: +``` +monorepo/ + app1/ + app2/ + comp1/ + comp2/ +``` + +The monorepo allows components to be separated from the app, providing: +* a level of encapsulation for components +* sharing of components + +### How to Set Up a Monorepo +Below expands on the monorepo structure above, adding the package.json files required to configure the monorepo for [yarn workspaces](https://yarnpkg.com/en/docs/workspaces). +``` +monorepo/ + package.json: + "workspaces": ["*"], + "private": true + app1/ + package.json: + "dependencies": ["@myorg/comp1": ">=0.0.0", "react": "^16.2.0"], + "devDependencies": ["react-scripts": "2.0.0"] + src/ + app.js: import comp1 from '@myorg/comp1'; + app2/ + package.json: + "dependencies": ["@myorg/comp1": ">=0.0.0", "react": "^16.2.0"], + "devDependencies": ["react-scripts": "2.0.0"] + src/ + app.js: import comp1 from '@myorg/comp1'; + comp1/ + package.json: + "name": "@myorg/comp1", + "version": "0.1.0" + index.js + comp2/ + package.json: + "name": "@myorg/comp2", + "version": "0.1.0", + "dependencies": ["@myorg/comp1": ">=0.0.0"], + "devDependencies": ["react": "^16.2.0"] + index.js: import comp1 from '@myorg/comp1' +``` +* Monorepo tools work on a package level, the same level as an npm package. +* The "workspaces" in the top-level package.json is an array of glob patterns specifying where shared packages are located in the monorepo. +* The scoping prefixes, e.g. @myorg/, are not required, but are recommended, allowing you to differentiate your packages from others of the same name. See [scoped packages ](https://docs.npmjs.com/misc/scope) for more info. +* Using a package in the monorepo is accomplished in the same manner as a published npm package, by specifying the shared package as dependency. +* In order to pick up the monorepo version of a package, the specified dependency version must semantically match the package version in the monorepo. See [semver](https://docs.npmjs.com/misc/semver) for info on semantic version matching. + +### CRA Apps in a Monorepo +* CRA apps in a monorepo are just a standard CRA app, they use the same react-script scripts. +* However, when you use react-scripts for an app in a monorepo, all packages in the monorepo are treated as app sources -- they are watched, linted, transpiled, and tested in the same way as if they were part of the app itself. +* Without this functionality, each package would need its own build/test/etc functionality and it would be challenging to link all of these together. + +### Lerna and Publishing +[Lerna](https://github.com/lerna/lerna) is a popular tool for managing monorepos. Lerna can be configured to use yarn workspaces, so it will work with the monorepo structure above. It's important to note that while lerna helps publish various packages in a monorepo, react-scripts does nothing to help publish a component to npm. A component which uses JSX or ES6+ features would need to be built by another tool before it can be published to npm. See [publishing components to npm](#publishing-components-to-npm) for more info. + ## Publishing Components to npm Create React App doesn't provide any built-in functionality to publish a component to npm. If you're ready to extract a component from your project so other people can use it, we recommend moving it to a separate directory outside of your project and then using a tool like [nwb](https://github.com/insin/nwb#react-components-and-libraries) to prepare it for publishing. diff --git a/tasks/e2e-kitchensink.sh b/tasks/e2e-kitchensink.sh index a1e1ccc9d42..36a6f5058d4 100755 --- a/tasks/e2e-kitchensink.sh +++ b/tasks/e2e-kitchensink.sh @@ -146,10 +146,6 @@ PORT=3001 \ nohup yarn start &>$tmp_server_log & grep -q 'You can now view' <(tail -f $tmp_server_log) -# Before running Mocha, specify that it should use our preset -# TODO: this is very hacky and we should find some other solution -echo '{"presets":["react-app"]}' > .babelrc - # Test "development" environment E2E_URL="http://localhost:3001" \ REACT_APP_SHELL_ENV_MESSAGE=fromtheshell \ @@ -166,10 +162,6 @@ E2E_FILE=./build/index.html \ PUBLIC_URL=http://www.example.org/spa/ \ node_modules/.bin/mocha --compilers js:@babel/register --require @babel/polyfill integration/*.test.js -# Remove the config we just created for Mocha -# TODO: this is very hacky and we should find some other solution -rm .babelrc - # ****************************************************************************** # Finally, let's check that everything still works after ejecting. # ****************************************************************************** diff --git a/tasks/e2e-monorepos.sh b/tasks/e2e-monorepos.sh new file mode 100755 index 00000000000..635badcf219 --- /dev/null +++ b/tasks/e2e-monorepos.sh @@ -0,0 +1,135 @@ +#!/bin/bash +# 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. + +# ****************************************************************************** +# This is an end-to-end test intended to run on CI. +# You can also run it locally but it's slow. +# ****************************************************************************** + +# Start in tasks/ even if run from root directory +cd "$(dirname "$0")" + +# App temporary location +# http://unix.stackexchange.com/a/84980 +temp_app_path=`mktemp -d 2>/dev/null || mktemp -d -t 'temp_app_path'` +custom_registry_url=http://localhost:4873 +original_npm_registry_url=`npm get registry` +original_yarn_registry_url=`yarn config get registry` + +function cleanup { + echo 'Cleaning up.' + cd "$root_path" + # Uncomment when snapshot testing is enabled by default: + # rm ./packages/react-scripts/template/src/__snapshots__/App.test.js.snap + rm -rf "$temp_app_path" + npm set registry "$original_npm_registry_url" + yarn config set registry "$original_yarn_registry_url" +} + +# Error messages are redirected to stderr +function handle_error { + echo "$(basename $0): ERROR! An error was encountered executing line $1." 1>&2; + cleanup + echo 'Exiting with error.' 1>&2; + exit 1 +} + +function handle_exit { + cleanup + echo 'Exiting without error.' 1>&2; + exit +} + +# Check for the existence of one or more files. +function exists { + for f in $*; do + test -e "$f" + done +} + +# Exit the script with a helpful error message when any error is encountered +trap 'set +x; handle_error $LINENO $BASH_COMMAND' ERR + +# Cleanup before exit on any termination signal +trap 'set +x; handle_exit' SIGQUIT SIGTERM SIGINT SIGKILL SIGHUP + +# Echo every command being executed +set -x + +# Go to root +cd .. +root_path=$PWD + +if hash npm 2>/dev/null +then + npm i -g npm@latest + npm cache clean || npm cache verify +fi + +# Bootstrap create-react-app monorepo +yarn + +# Start local registry +tmp_registry_log=`mktemp` +nohup npx verdaccio@2.7.2 &>$tmp_registry_log & +# Wait for `verdaccio` to boot +grep -q 'http address' <(tail -f $tmp_registry_log) + +# Set registry to local registry +npm set registry "$custom_registry_url" +yarn config set registry "$custom_registry_url" + +# Login so we can publish packages +npx npm-cli-login@0.0.10 -u user -p password -e user@example.com -r "$custom_registry_url" --quotes + +git clean -df +./tasks/publish.sh --yes --force-publish=* --skip-git --cd-version=prerelease --exact --npm-tag=latest + +function verifyTest { + CI=true yarn test --watch=no --json --outputFile testoutput.json || return 1 + cat testoutput.json + # on windows, output contains double backslashes for path separator + grep -E -q "src([\\]{1,2}|/)App.test.js" testoutput.json || return 1 + grep -E -q "comp1([\\]{1,2}|/)index.test.js" testoutput.json || return 1 + grep -E -q "comp2([\\]{1,2}|/)index.test.js" testoutput.json || return 1 +} + +function verifyBuild { + yarn build || return 1 + grep -F -R --exclude=*.map "YarnWS-CraApp" build/ -q || return 1 +} + +# ****************************************************************************** +# Test yarn workspace monorepo +# ****************************************************************************** +# Set up yarn workspace monorepo +pushd "$temp_app_path" +cp -r "$root_path/packages/react-scripts/fixtures/monorepos/yarn-ws" . +cd "yarn-ws" +cp -r "$root_path/packages/react-scripts/fixtures/monorepos/packages" . +yarn + +# Test cra-app1 +cd packages/cra-app1 +verifyBuild +yarn start --smoke-test +verifyTest + +# Test eject +echo yes | npm run eject +verifyBuild +yarn start --smoke-test +verifyTest + +# ****************************************************************************** +# Test create-react-app inside workspace +# ****************************************************************************** +# npx create-react-app --internal-testing-template="$root_path"/packages/react-scripts/fixtures/yarn-ws/ws/cra-app1 cra-app2 +# -- above needs https://github.com/facebookincubator/create-react-app/pull/3435 to user create-react-app +popd + +# Cleanup +cleanup diff --git a/tasks/local-test.sh b/tasks/local-test.sh index 0416fb5d9cc..dc70b3b2f19 100755 --- a/tasks/local-test.sh +++ b/tasks/local-test.sh @@ -49,7 +49,7 @@ while [ "$1" != "" ]; do shift done -test_command="./tasks/e2e-simple.sh && ./tasks/e2e-kitchensink.sh && ./tasks/e2e-installs.sh" +test_command="./tasks/e2e-simple.sh && ./tasks/e2e-kitchensink.sh && ./tasks/e2e-installs.sh && ./tasks/e2e-monorepos.sh" case ${test_suite} in "all") ;; @@ -62,6 +62,9 @@ case ${test_suite} in "installs") test_command="./tasks/e2e-installs.sh" ;; + "monorepos") + test_command="./tasks/e2e-monorepos.sh" + ;; *) ;; esac