Skip to content

Commit 137cb2a

Browse files
committed
Add support yarn workspaces
1 parent 8b0dd54 commit 137cb2a

File tree

3 files changed

+306
-3
lines changed

3 files changed

+306
-3
lines changed

packages/react-scripts/config/paths.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
const path = require('path');
1212
const fs = require('fs');
13+
const findUp = require('find-up');
1314
const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath');
1415

1516
// Make sure any symlinks in the project folder are resolved:
@@ -103,7 +104,9 @@ module.exports = {
103104
};
104105

105106
const ownPackageJson = require('../package.json');
106-
const reactScriptsPath = resolveApp(`node_modules/${ownPackageJson.name}`);
107+
const reactScriptsPath = findUp.sync(`node_modules/${ownPackageJson.name}`, {
108+
cwd: resolveApp('.'),
109+
});
107110
const reactScriptsLinked =
108111
fs.existsSync(reactScriptsPath) &&
109112
fs.lstatSync(reactScriptsPath).isSymbolicLink();

packages/react-scripts/config/webpack.config.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent')
2929
const paths = require('./paths');
3030
const modules = require('./modules');
3131
const getClientEnvironment = require('./env');
32+
const yarnWorkspaces = require('./yarn-workspaces');
3233
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
3334
const ForkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin');
3435
const typescriptFormatter = require('react-dev-utils/typescriptFormatter');
@@ -59,6 +60,8 @@ const cssModuleRegex = /\.module\.css$/;
5960
const sassRegex = /\.(scss|sass)$/;
6061
const sassModuleRegex = /\.module\.(scss|sass)$/;
6162

63+
const workspacesConfig = yarnWorkspaces.init(paths);
64+
6265
// This is the production and development configuration.
6366
// It is focused on developer experience, fast rebuilds, and a minimal bundle.
6467
module.exports = function(webpackEnv) {
@@ -70,6 +73,27 @@ module.exports = function(webpackEnv) {
7073
const isEnvProductionProfile =
7174
isEnvProduction && process.argv.includes('--profile');
7275

76+
const workspacesMainFields = [
77+
workspacesConfig.packageEntry,
78+
'browser',
79+
'module',
80+
'main',
81+
];
82+
83+
const mainFields =
84+
isEnvDevelopment && workspacesConfig.development
85+
? workspacesMainFields
86+
: isEnvProduction && workspacesConfig.production
87+
? workspacesMainFields
88+
: undefined;
89+
90+
const includePaths =
91+
isEnvDevelopment && workspacesConfig.development
92+
? [paths.appSrc, ...workspacesConfig.paths]
93+
: isEnvProduction && workspacesConfig.production
94+
? [paths.appSrc, ...workspacesConfig.paths]
95+
: paths.appSrc;
96+
7397
// We will provide `paths.publicUrlOrPath` to our app
7498
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
7599
// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
@@ -297,6 +321,7 @@ module.exports = function(webpackEnv) {
297321
extensions: paths.moduleFileExtensions
298322
.map(ext => `.${ext}`)
299323
.filter(ext => useTypeScript || !ext.includes('ts')),
324+
mainFields,
300325
alias: {
301326
// Support React Native Web
302327
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
@@ -373,7 +398,7 @@ module.exports = function(webpackEnv) {
373398
loader: require.resolve('eslint-loader'),
374399
},
375400
],
376-
include: paths.appSrc,
401+
include: includePaths,
377402
},
378403
{
379404
// "oneOf" will traverse all following loaders until one will
@@ -395,7 +420,7 @@ module.exports = function(webpackEnv) {
395420
// The preset includes JSX, Flow, TypeScript, and some ESnext features.
396421
{
397422
test: /\.(js|mjs|jsx|ts|tsx)$/,
398-
include: paths.appSrc,
423+
include: includePaths,
399424
loader: require.resolve('babel-loader'),
400425
options: {
401426
customize: require.resolve(
@@ -709,6 +734,7 @@ module.exports = function(webpackEnv) {
709734
'!**/src/setupProxy.*',
710735
'!**/src/setupTests.*',
711736
],
737+
watch: includePaths,
712738
silent: true,
713739
// The formatter is invoked directly in WebpackDevServerUtils during development
714740
formatter: isEnvProduction ? typescriptFormatter : undefined,
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
'use strict';
2+
3+
const fse = require('fs-extra');
4+
const path = require('path');
5+
const findUp = require('find-up');
6+
const glob = require('glob');
7+
8+
const loadPackageJson = packagePath => {
9+
try {
10+
const packageObj = fse.readJsonSync(packagePath);
11+
return packageObj;
12+
} catch (err) {
13+
throw err;
14+
}
15+
};
16+
17+
const getWorkspacesRootConfig = dir => {
18+
const packageJsonUp = findUp.sync('package.json', { cwd: dir });
19+
20+
if (packageJsonUp === null) {
21+
return false;
22+
}
23+
24+
const packageObj = loadPackageJson(packageJsonUp);
25+
26+
if (
27+
packageObj.workspaces &&
28+
(Array.isArray(packageObj.workspaces) ||
29+
Reflect.has(packageObj.workspaces, 'packages'))
30+
) {
31+
const workspacesRootConfig = {
32+
root: path.dirname(packageJsonUp),
33+
workspaces: packageObj.workspaces,
34+
};
35+
return workspacesRootConfig;
36+
}
37+
38+
const dirUp = path.dirname(dir);
39+
return getWorkspacesRootConfig(dirUp);
40+
};
41+
42+
const getPackagePaths = (root, workspacesList) => {
43+
const packageList = [];
44+
45+
workspacesList.forEach(workspace => {
46+
const workspaceDir = path.dirname(workspace);
47+
const workspaceAbsDir = path.join(root, workspaceDir);
48+
const packageJsonGlob = path.join('**!(node_modules)', 'package.json');
49+
const packageJsonAbsPaths = glob
50+
.sync(packageJsonGlob, { cwd: workspaceAbsDir })
51+
.map(pkgPath => path.join(workspaceAbsDir, pkgPath));
52+
53+
packageList.push(...packageJsonAbsPaths);
54+
});
55+
56+
return packageList;
57+
};
58+
59+
const getDeep = (obj, keyChain) => {
60+
const nextKey = keyChain.shift();
61+
const has = Reflect.has(obj, nextKey);
62+
const val = obj[nextKey];
63+
64+
if (keyChain.length === 0) {
65+
return val;
66+
}
67+
68+
if (has) {
69+
return getDeep(val, keyChain);
70+
}
71+
72+
return false;
73+
};
74+
75+
const resolveBabelLoaderPaths = ({ root, workspacesList }, packageEntry) => {
76+
const packageJsonPaths = getPackagePaths(root, workspacesList);
77+
const babelLoaderPaths = [];
78+
79+
packageJsonPaths.map(absPkgPath => {
80+
const packageJson = loadPackageJson(absPkgPath);
81+
const mainSrcFile = getDeep(packageJson, [packageEntry]);
82+
83+
if (mainSrcFile) {
84+
const mainSrcPath = path.dirname(mainSrcFile);
85+
const packageAbsDir = path.dirname(absPkgPath);
86+
const absSrcPath = path.join(packageAbsDir, mainSrcPath);
87+
babelLoaderPaths.push(absSrcPath);
88+
}
89+
});
90+
91+
return babelLoaderPaths;
92+
};
93+
94+
const loadAppSettings = appPackageJson => {
95+
const result = { workspaces: {}, dependencies: {} };
96+
97+
const appPackageObj = loadPackageJson(appPackageJson);
98+
99+
const dependencies = getDeep(appPackageObj, ['dependencies']);
100+
const devDependencies = getDeep(appPackageObj, ['devDependencies']);
101+
102+
if (!dependencies && !devDependencies) {
103+
return result;
104+
}
105+
106+
if (dependencies) {
107+
result.dependencies = Object.assign(result.dependencies, dependencies);
108+
}
109+
110+
if (devDependencies) {
111+
result.dependencies = Object.assign(result.dependencies, devDependencies);
112+
}
113+
114+
const reactScripts = getDeep(appPackageObj, ['react-scripts']);
115+
if (!reactScripts) {
116+
return result;
117+
}
118+
119+
const workspaces = getDeep(reactScripts, ['workspaces']);
120+
result.workspaces = workspaces;
121+
if (!workspaces) {
122+
return result;
123+
}
124+
125+
return workspaces;
126+
};
127+
128+
const guard = (appDirectory, appPackageJson) => {
129+
if (!appDirectory) {
130+
throw new Error('appDirectory not provided');
131+
}
132+
133+
if (typeof appDirectory !== 'string') {
134+
throw new Error('appDirectory should be a string');
135+
}
136+
137+
if (!appPackageJson) {
138+
throw new Error('appPackageJson not provided');
139+
}
140+
141+
if (typeof appPackageJson !== 'string') {
142+
throw new Error('appPackageJson should be a string');
143+
}
144+
};
145+
146+
const getPkg = path => {
147+
const pkgPath = findUp.sync('package.json', { cwd: path });
148+
const pkg = loadPackageJson(pkgPath);
149+
return pkg;
150+
};
151+
152+
const getDeps = pkg => {
153+
const deps = getDeep(pkg, ['dependencies']);
154+
const devDeps = getDeep(pkg, ['devDependencies']);
155+
156+
let dependencies = {};
157+
158+
if (deps) {
159+
dependencies = Object.assign(dependencies, deps);
160+
}
161+
162+
if (devDeps) {
163+
dependencies = Object.assign(dependencies, devDeps);
164+
}
165+
166+
return dependencies;
167+
};
168+
169+
const depsTable = {};
170+
171+
const buildDepsTable = srcPaths => {
172+
srcPaths.forEach(path => {
173+
const pkg = getPkg(path);
174+
const name = pkg.name;
175+
const deps = getDeps(pkg);
176+
depsTable[name] = { path, deps };
177+
});
178+
};
179+
180+
const filterSrcPaths = (srcPaths, dependencies) => {
181+
const filteredPaths = [];
182+
183+
srcPaths.forEach(path => {
184+
const pkg = getPkg(path);
185+
186+
if (dependencies && Reflect.has(dependencies, pkg.name)) {
187+
filteredPaths.push(path);
188+
189+
const subDeps = depsTable[pkg.name].deps;
190+
const subPaths = filterSrcPaths(srcPaths, subDeps);
191+
filteredPaths.push(...subPaths);
192+
}
193+
});
194+
195+
return filteredPaths;
196+
};
197+
198+
const init = paths => {
199+
guard(paths.appPath, paths.appPackageJson);
200+
201+
const config = {
202+
root: null,
203+
paths: [],
204+
packageEntry: 'main:src',
205+
development: true,
206+
production: true,
207+
};
208+
209+
const { root, workspaces } = getWorkspacesRootConfig(paths.appPath);
210+
const workspacesList = [];
211+
212+
// Normally "workspaces" in package.json is an array
213+
if (Array.isArray(workspaces)) {
214+
workspacesList.push(...workspaces);
215+
}
216+
217+
// Sometimes "workspaces" in package.json is an object
218+
// with a ".packages" sub-array, eg: when used with "nohoist"
219+
// See: https://yarnpkg.com/blog/2018/02/15/nohoist
220+
if (
221+
workspaces &&
222+
!Array.isArray(workspaces) &&
223+
Reflect.has(workspaces, 'packages')
224+
) {
225+
workspacesList.push(...workspaces.packages);
226+
}
227+
228+
if (workspacesList.length === 0) {
229+
return config;
230+
}
231+
console.log('Yarn Workspaces paths detected.');
232+
config.root = root;
233+
234+
const appSettings = loadAppSettings(paths.appPackageJson);
235+
236+
if (Reflect.has(appSettings.workspaces, 'development')) {
237+
config.development = appSettings.workspaces.development ? true : false;
238+
}
239+
240+
if (Reflect.has(appSettings.workspaces, 'production')) {
241+
config.production = appSettings.workspaces.production ? true : false;
242+
}
243+
244+
if (Reflect.has(appSettings.workspaces, 'package-entry')) {
245+
config.packageEntry = appSettings.workspaces['package-entry'];
246+
}
247+
248+
const babelSrcPaths = resolveBabelLoaderPaths(
249+
{ root, workspacesList },
250+
config.packageEntry
251+
);
252+
253+
buildDepsTable(babelSrcPaths);
254+
255+
const applicableSrcPaths = [
256+
...new Set(filterSrcPaths(babelSrcPaths, appSettings.dependencies)),
257+
];
258+
259+
console.log(
260+
`Found ${babelSrcPaths.length} path(s) with "${config.packageEntry}" entry.`
261+
);
262+
263+
if (applicableSrcPaths.length > 0) {
264+
config.paths.push(...applicableSrcPaths);
265+
}
266+
267+
console.log('Exporting Workspaces config to Webpack.');
268+
console.log(config);
269+
return config;
270+
};
271+
272+
module.exports = {
273+
init,
274+
};

0 commit comments

Comments
 (0)