-
-
Notifications
You must be signed in to change notification settings - Fork 26.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for yarn and lerna monorepos. #3741
Changes from 27 commits
bf2d814
d9f5cf7
12e76ad
bd1124d
eae24e9
f4b3a0d
739b59b
7227e5d
a93573d
8102907
9a1b92c
f4f2882
d8e0319
f96d04c
565c1d7
75ae0c5
fbc6bde
21f0b00
9feda8b
8ab33c7
d52e904
79ac815
3e81144
ec6f5a8
978674f
fc9b890
4d0bc68
9e7490c
50b4666
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,45 @@ 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) { | ||
// Yarn workspace | ||
let pkgPatterns = require(monoPkgPath).workspaces; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yarn workspaces can coexist with lerna. That scenario needs to be handled. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For lerna+YW (useWorkspaces), lerna uses the same config as YW itself (package.workspaces), so I think that case is handled correctly here. For the case where lerna is not configured to useWorkspaces, but there is a packages.workspaces, this logic would choose the package.workspaces config, but I don't think that's really a valid use case. In summary, I think this logic is ok as-is, I'll add a comment to clear it up, give me more details if I'm missing something. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. perfect, I just want to make sure this part. There could potentially be no use cases. But have to be careful and you can do this kind of things. |
||
if (!pkgPatterns) { | ||
// lerna | ||
const lernaJson = path.resolve(path.dirname(monoPkgPath), 'lerna.json'); | ||
pkgPatterns = fs.existsSync(lernaJson) && require(lernaJson).packages; | ||
} | ||
const pkgPaths = findPkgs(path.dirname(monoPkgPath), pkgPatterns); | ||
// check if app is part of monorepo | ||
if (pkgPaths.indexOf(appDirectory) !== -1) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we add this filter above |
||
return pkgPaths.filter(f => fs.realpathSync(f) !== appDirectory); | ||
} | ||
} | ||
return []; | ||
}; | ||
|
||
if (checkForMonorepo) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we merge There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The useTemplate code is removed when ejecting. I'm sure there are other ways to do this logic, but not sure there is a clearly cleaner way ... if you have a way you think is clearly cleaner, send it in, I'll take it :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. move the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. change the variable name as needed. |
||
// 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()); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -145,18 +145,19 @@ 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 | ||
}, | ||
loader: require.resolve('eslint-loader'), | ||
}, | ||
], | ||
include: paths.appSrc, | ||
include: paths.srcPaths, | ||
exclude: [/[/\\\\]node_modules[/\\\\]/], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you please explain why this has become necessary? Is this because non-CRA packages have their source code at the same level of nesting as their own There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, since srcPaths includes comp1 root (or or top-level monorepo root, depending on decision above), both comp1/file.js and comp1/node_modules/somedep/somefile.js get included in the include filter, so we need to filter out node_modules. (Originally, only app/src was included, so there was no need to filter out app/node_modules.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Am not sure this is getting exclufed. I can still find node_modules inside the comp1 that is inside app3. Doesnt it completely exclude nodemodules? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These webpack excludes just exclude them from getting transpiled and linted. |
||
}, | ||
{ | ||
// "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, | ||
}, | ||
}, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"presets": ["react-app"] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
'use strict'; | ||
|
||
const { execSync } = require('child_process'); | ||
const { resolve } = require('path'); | ||
const { existsSync } = require('fs'); | ||
const { platform } = require('os'); | ||
const crossSpawn = require('cross-spawn'); | ||
|
||
// function shouldUseYarn() { | ||
// try { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What’s up with this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is to force npm as the client (as also specified in lerna.json) so that lerna bootstrap --hoist works. This test case is hoisting react which allows the shared components to pick it up without actually specifying it as a dependency -- which is handy, but probably bad and will be broken by #1752. I originally did that to avoid components having to specify react and match version with the app for fear that mismatched versions would cause issues. (Note: YW automatically hoists, so this is partially matching that behavior.) But maybe this should be approached some other way. Ideas? Is it ideal for shared components to specify react as a peerDependency and match the app react version? |
||
// execSync('yarnpkg --version', { stdio: 'ignore' }); | ||
// return true; | ||
// } catch (e) { | ||
// return false; | ||
// } | ||
// } | ||
|
||
function shouldUseNpmConcurrently() { | ||
try { | ||
const versionString = execSync('npm --version'); | ||
const m = /^(\d+)[.]/.exec(versionString); | ||
// NPM >= 5 support concurrent installs | ||
return Number(m[1]) >= 5; | ||
} catch (e) { | ||
return false; | ||
} | ||
} | ||
|
||
// const yarn = shouldUseYarn(); | ||
const yarn = false; | ||
const windows = platform() === 'win32'; | ||
const lerna = resolve( | ||
__dirname, | ||
'node_modules', | ||
'.bin', | ||
windows ? 'lerna.cmd' : 'lerna' | ||
); | ||
|
||
if (!existsSync(lerna)) { | ||
if (yarn) { | ||
console.log('Cannot find lerna. Please run `yarn --check-files`.'); | ||
} else { | ||
console.log( | ||
'Cannot find lerna. Please remove `node_modules` and run `npm install`.' | ||
); | ||
} | ||
process.exit(1); | ||
} | ||
|
||
let args = ['bootstrap'].concat(process.argv.slice(2)); | ||
if (yarn) { | ||
// Yarn does not support concurrency | ||
crossSpawn.sync( | ||
lerna, | ||
args.concat(['--npm-client=yarn', '--concurrency=1']), | ||
{ | ||
stdio: 'inherit', | ||
} | ||
); | ||
} else { | ||
if ( | ||
// The Windows filesystem does not handle concurrency well | ||
windows || | ||
// Only newer npm versions support concurrency | ||
!shouldUseNpmConcurrently() | ||
) { | ||
args.push('--concurrency=1'); | ||
} | ||
crossSpawn.sync(lerna, args, { stdio: 'inherit' }); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"lerna": "2.6.0", | ||
"packages": [ | ||
"packages/*" | ||
], | ||
"version": "0.0.0" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"devDependencies": { | ||
"cross-spawn": "6.0.3", | ||
"lerna": "2.6.0" | ||
}, | ||
"scripts": { | ||
"postinstall": "node bootstrap.js --hoist \"@(react|react-dom)\"" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import React from 'react'; | ||
|
||
const Comp1 = () => <div>Comp1</div>; | ||
|
||
export default Comp1; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(<Comp1 />, div); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"name": "comp1", | ||
"version": "1.0.0", | ||
"main": "index.js", | ||
"license": "MIT" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import React from 'react'; | ||
|
||
import Comp1 from 'comp1'; | ||
|
||
const Comp2 = () => ( | ||
<div> | ||
Comp2, nested Comp1: <Comp1 /> | ||
</div> | ||
); | ||
|
||
export default Comp2; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(<Comp2 />, div); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"name": "comp2", | ||
"dependencies": { | ||
"comp1": "^1.0.0" | ||
}, | ||
"version": "1.0.0", | ||
"main": "index.js", | ||
"license": "MIT" | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we still create babelrc on eject? If we do I think it might be confusing that removing
babel-preset-react-app
from it won't actually remove it (because it's also inlined here and in webpack config).Maybe we don't need to create babelrc on eject then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, babel config still gets written to package.json on eject. Yes, it could be confusing, and probably should not be there. Will remove it. Same thing for eslint config.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FTR, removing babel config from package.json impacted mocha tests that run in e2e-kitchensink. Babelrc was already being created in the e2e script itself, but only for tests before eject. I opted to fix this by adding .babelrc to the fixture and removing the part of e2e script that was creating it.