Skip to content

Commit 774565a

Browse files
bradfordlemleygaearon
authored andcommitted
Add support for yarn and lerna monorepos. (facebook#3741)
* Support for multiple source paths via package.json srcPaths entry. Initial support for yarn workspace. Support lerna workspace, fix for when to use template files. Remove support for specifying srcPaths in package.json. Re-enable transpilation caching. * Clean up, use file matching (similar to original) in webpack configs instead of matching function. * Remove package lock files. * Fix for eject. Note: monorepos won't work after eject. Can be fixed easily with JEST 22.0.?+ which has file pattern matches against realpaths. * Filter tests to run only tests in monorepo components included by the app. (Not sure this is desireable, might be cool to be able to easily run all tests in monorepo from one app.) * Fix conditions for when to use template. * Fix eject. * Remove code that is not needed w/ Jest 22. * Include all cra-comp tests in monorepo instead of trying to include only tests that are dependencies of app. (tests can be easily filtered via jest cli if desired, e.g. 'npm test -- myapp comp1') * Pin find-pkg version. * Hopefully fix jest test file matching on windows by removing first slash. * E2E tests for monorepo. * Run monorepo tests in CI. * Fix and test post-eject build. * Fix e2e test. * Fix test suite names in appveyor. * Include individual package dirs as srcPaths instead of top-level monorepo root. Fixes build/start after eject. * Fix running tests after eject. * Clean up test workspace, add some verifcations. * Cleanup. * Try to fix hang when running test on appveyor. * Don't write babel or lint config to package.json when ejecting. * Incorporate review comments. * Simply monorepo pkg finder * Only include monorepo pkgs if app itself is included in monorepo * Check for specific tests in e2e * Fixes for windows. * Fix for kitchensink mocha tests compiling. * Add lerna monorepo test. * Fix lerna bootstrap on windows. * Incorporate more review comments: * remove support for lerna w/o yarn workspace * add react and react-dom as devDeps to comp1 and comp2 * Add monorepo info to user guide.
1 parent 659446f commit 774565a

28 files changed

+390
-42
lines changed

config/jest/babelTransform.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
// @remove-file-on-eject
1+
// @remove-on-eject-begin
22
/**
33
* Copyright (c) 2014-present, Facebook, Inc.
44
*
55
* This source code is licensed under the MIT license found in the
66
* LICENSE file in the root directory of this source tree.
77
*/
8+
// @remove-on-eject-end
89
'use strict';
910

1011
const babelJest = require('babel-jest');
1112

1213
module.exports = babelJest.createTransformer({
1314
presets: [require.resolve('babel-preset-react-app')],
15+
// @remove-on-eject-begin
1416
babelrc: false,
17+
// @remove-on-eject-end
1518
});

config/paths.js

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
const path = require('path');
1212
const fs = require('fs');
1313
const url = require('url');
14+
const findPkg = require('find-pkg');
15+
const globby = require('globby');
1416

1517
// Make sure any symlinks in the project folder are resolved:
1618
// https://github.com/facebook/create-react-app/issues/637
@@ -63,6 +65,8 @@ module.exports = {
6365
servedPath: getServedPath(resolveApp('package.json')),
6466
};
6567

68+
let checkForMonorepo = true;
69+
6670
// @remove-on-eject-begin
6771
const resolveOwn = relativePath => path.resolve(__dirname, '..', relativePath);
6872

@@ -86,17 +90,13 @@ module.exports = {
8690
ownNodeModules: resolveOwn('node_modules'), // This is empty on npm 3
8791
};
8892

89-
const ownPackageJson = require('../package.json');
90-
const reactScriptsPath = resolveApp(`node_modules/${ownPackageJson.name}`);
91-
const reactScriptsLinked =
92-
fs.existsSync(reactScriptsPath) &&
93-
fs.lstatSync(reactScriptsPath).isSymbolicLink();
94-
95-
// config before publish: we're in ./packages/react-scripts/config/
96-
if (
97-
!reactScriptsLinked &&
98-
__dirname.indexOf(path.join('packages', 'react-scripts', 'config')) !== -1
99-
) {
93+
// detect if template should be used, ie. when cwd is react-scripts itself
94+
const useTemplate =
95+
appDirectory === fs.realpathSync(path.join(__dirname, '..'));
96+
97+
checkForMonorepo = !useTemplate;
98+
99+
if (useTemplate) {
100100
module.exports = {
101101
dotenv: resolveOwn('template/.env'),
102102
appPath: resolveApp('.'),
@@ -117,3 +117,40 @@ if (
117117
};
118118
}
119119
// @remove-on-eject-end
120+
121+
module.exports.srcPaths = [module.exports.appSrc];
122+
123+
const findPkgs = (rootPath, globPatterns) => {
124+
const globOpts = {
125+
cwd: rootPath,
126+
strict: true,
127+
absolute: true,
128+
};
129+
return globPatterns
130+
.reduce(
131+
(pkgs, pattern) =>
132+
pkgs.concat(globby.sync(path.join(pattern, 'package.json'), globOpts)),
133+
[]
134+
)
135+
.map(f => path.dirname(path.normalize(f)));
136+
};
137+
138+
const getMonorepoPkgPaths = () => {
139+
const monoPkgPath = findPkg.sync(path.resolve(appDirectory, '..'));
140+
if (monoPkgPath) {
141+
// get monorepo config from yarn workspace
142+
const pkgPatterns = require(monoPkgPath).workspaces;
143+
const pkgPaths = findPkgs(path.dirname(monoPkgPath), pkgPatterns);
144+
// only include monorepo pkgs if app itself is included in monorepo
145+
if (pkgPaths.indexOf(appDirectory) !== -1) {
146+
return pkgPaths.filter(f => fs.realpathSync(f) !== appDirectory);
147+
}
148+
}
149+
return [];
150+
};
151+
152+
if (checkForMonorepo) {
153+
// if app is in a monorepo (lerna or yarn workspace), treat other packages in
154+
// the monorepo as if they are app source
155+
Array.prototype.push.apply(module.exports.srcPaths, getMonorepoPkgPaths());
156+
}

config/webpack.config.dev.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,18 +145,19 @@ module.exports = {
145145
options: {
146146
formatter: eslintFormatter,
147147
eslintPath: require.resolve('eslint'),
148-
// @remove-on-eject-begin
149148
baseConfig: {
150149
extends: [require.resolve('eslint-config-react-app')],
151150
},
151+
// @remove-on-eject-begin
152152
ignore: false,
153153
useEslintrc: false,
154154
// @remove-on-eject-end
155155
},
156156
loader: require.resolve('eslint-loader'),
157157
},
158158
],
159-
include: paths.appSrc,
159+
include: paths.srcPaths,
160+
exclude: [/[/\\\\]node_modules[/\\\\]/],
160161
},
161162
{
162163
// "oneOf" will traverse all following loaders until one will
@@ -178,7 +179,8 @@ module.exports = {
178179
// The preset includes JSX, Flow, and some ESnext features.
179180
{
180181
test: /\.(js|jsx|mjs)$/,
181-
include: paths.appSrc,
182+
include: paths.srcPaths,
183+
exclude: [/[/\\\\]node_modules[/\\\\]/],
182184
use: [
183185
// This loader parallelizes code compilation, it is optional but
184186
// improves compile time on larger projects
@@ -188,8 +190,8 @@ module.exports = {
188190
options: {
189191
// @remove-on-eject-begin
190192
babelrc: false,
191-
presets: [require.resolve('babel-preset-react-app')],
192193
// @remove-on-eject-end
194+
presets: [require.resolve('babel-preset-react-app')],
193195
// This is a feature of `babel-loader` for webpack (not Babel itself).
194196
// It enables caching results in ./node_modules/.cache/babel-loader/
195197
// directory for faster rebuilds.
@@ -275,8 +277,8 @@ module.exports = {
275277
options: {
276278
// @remove-on-eject-begin
277279
babelrc: false,
278-
presets: [require.resolve('babel-preset-react-app')],
279280
// @remove-on-eject-end
281+
presets: [require.resolve('babel-preset-react-app')],
280282
cacheDirectory: true,
281283
},
282284
},

config/webpack.config.prod.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,20 +152,21 @@ module.exports = {
152152
options: {
153153
formatter: eslintFormatter,
154154
eslintPath: require.resolve('eslint'),
155-
// @remove-on-eject-begin
156155
// TODO: consider separate config for production,
157156
// e.g. to enable no-console and no-debugger only in production.
158157
baseConfig: {
159158
extends: [require.resolve('eslint-config-react-app')],
160159
},
160+
// @remove-on-eject-begin
161161
ignore: false,
162162
useEslintrc: false,
163163
// @remove-on-eject-end
164164
},
165165
loader: require.resolve('eslint-loader'),
166166
},
167167
],
168-
include: paths.appSrc,
168+
include: paths.srcPaths,
169+
exclude: [/[/\\\\]node_modules[/\\\\]/],
169170
},
170171
{
171172
// "oneOf" will traverse all following loaders until one will
@@ -186,7 +187,8 @@ module.exports = {
186187
// The preset includes JSX, Flow, and some ESnext features.
187188
{
188189
test: /\.(js|jsx|mjs)$/,
189-
include: paths.appSrc,
190+
include: paths.srcPaths,
191+
exclude: [/[/\\\\]node_modules[/\\\\]/],
190192
use: [
191193
// This loader parallelizes code compilation, it is optional but
192194
// improves compile time on larger projects
@@ -196,8 +198,8 @@ module.exports = {
196198
options: {
197199
// @remove-on-eject-begin
198200
babelrc: false,
199-
presets: [require.resolve('babel-preset-react-app')],
200201
// @remove-on-eject-end
202+
presets: [require.resolve('babel-preset-react-app')],
201203
compact: true,
202204
highlightCode: true,
203205
},
@@ -317,8 +319,8 @@ module.exports = {
317319
options: {
318320
// @remove-on-eject-begin
319321
babelrc: false,
320-
presets: [require.resolve('babel-preset-react-app')],
321322
// @remove-on-eject-end
323+
presets: [require.resolve('babel-preset-react-app')],
322324
cacheDirectory: true,
323325
},
324326
},

fixtures/kitchensink/.babelrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"presets": ["react-app"]
3+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import React from 'react';
2+
3+
const Comp1 = () => <div>Comp1</div>;
4+
5+
export default Comp1;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import React from 'react';
2+
import ReactDOM from 'react-dom';
3+
import Comp1 from '.';
4+
5+
it('renders Comp1 without crashing', () => {
6+
const div = document.createElement('div');
7+
ReactDOM.render(<Comp1 />, div);
8+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "comp1",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"license": "MIT",
6+
"devDependencies": {
7+
"react": "^16.2.0",
8+
"react-dom": "^16.2.0"
9+
}
10+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import React from 'react';
2+
3+
import Comp1 from 'comp1';
4+
5+
const Comp2 = () => (
6+
<div>
7+
Comp2, nested Comp1: <Comp1 />
8+
</div>
9+
);
10+
11+
export default Comp2;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import React from 'react';
2+
import ReactDOM from 'react-dom';
3+
import Comp2 from '.';
4+
5+
it('renders Comp2 without crashing', () => {
6+
const div = document.createElement('div');
7+
ReactDOM.render(<Comp2 />, div);
8+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "comp2",
3+
"dependencies": {
4+
"comp1": "^1.0.0"
5+
},
6+
"devDependencies": {
7+
"react": "^16.2.0",
8+
"react-dom": "^16.2.0"
9+
},
10+
"version": "1.0.0",
11+
"main": "index.js",
12+
"license": "MIT"
13+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# See https://help.github.com/ignore-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
6+
# testing
7+
/coverage
8+
9+
# production
10+
/build
11+
12+
# misc
13+
.DS_Store
14+
.env.local
15+
.env.development.local
16+
.env.test.local
17+
.env.production.local
18+
19+
npm-debug.log*
20+
yarn-debug.log*
21+
yarn-error.log*
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "cra-app1",
3+
"version": "0.1.0",
4+
"private": true,
5+
"dependencies": {
6+
"comp2": "^1.0.0",
7+
"react": "^16.2.0",
8+
"react-dom": "^16.2.0"
9+
},
10+
"devDependencies": {
11+
"react-scripts": "latest"
12+
},
13+
"scripts": {
14+
"start": "react-scripts start",
15+
"build": "react-scripts build",
16+
"test": "react-scripts test --env=jsdom",
17+
"eject": "react-scripts eject"
18+
},
19+
"browserslist": {
20+
"development": [
21+
"last 2 chrome versions",
22+
"last 2 firefox versions",
23+
"last 2 edge versions"
24+
],
25+
"production": [
26+
">1%",
27+
"last 4 versions",
28+
"Firefox ESR",
29+
"not ie < 11"
30+
]
31+
}
32+
}
Binary file not shown.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6+
<meta name="theme-color" content="#000000">
7+
<!--
8+
manifest.json provides metadata used when your web app is added to the
9+
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
10+
-->
11+
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
12+
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
13+
<!--
14+
Notice the use of %PUBLIC_URL% in the tags above.
15+
It will be replaced with the URL of the `public` folder during the build.
16+
Only files inside the `public` folder can be referenced from the HTML.
17+
18+
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
19+
work correctly both with client-side routing and a non-root public URL.
20+
Learn how to configure a non-root public URL by running `npm run build`.
21+
-->
22+
<title>React App</title>
23+
</head>
24+
<body>
25+
<noscript>
26+
You need to enable JavaScript to run this app.
27+
</noscript>
28+
<div id="root"></div>
29+
<!--
30+
This HTML file is a template.
31+
If you open it directly in the browser, you will see an empty page.
32+
33+
You can add webfonts, meta tags, or analytics to this file.
34+
The build step will place the bundled scripts into the <body> tag.
35+
36+
To begin the development, run `npm start` or `yarn start`.
37+
To create a production bundle, use `npm run build` or `yarn build`.
38+
-->
39+
</body>
40+
</html>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"short_name": "React App",
3+
"name": "Create React App Sample",
4+
"icons": [
5+
{
6+
"src": "favicon.ico",
7+
"sizes": "64x64 32x32 24x24 16x16",
8+
"type": "image/x-icon"
9+
}
10+
],
11+
"start_url": "./index.html",
12+
"display": "standalone",
13+
"theme_color": "#000000",
14+
"background_color": "#ffffff"
15+
}

0 commit comments

Comments
 (0)