Skip to content

Commit a86f49f

Browse files
committed
Merge pull request expo#503 from connectdotz/yarn-lookup-fix
fixed using wrong package manager in yarn workspace projects
2 parents e531d1a + ed1603c commit a86f49f

File tree

10 files changed

+2564
-31
lines changed

10 files changed

+2564
-31
lines changed

package.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
{
22
"private": true,
33
"scripts": {
4-
"format": "prettier --single-quote --trailing-comma=es5 --print-width=100 --write '{create-react-native-app,react-native-scripts}/{src,template}/**/*.js'"
4+
"format": "prettier --single-quote --trailing-comma=es5 --print-width=100 --write '{create-react-native-app,react-native-scripts}/{src,template}/**/*.js'",
5+
"test": "jest"
56
},
67
"devDependencies": {
8+
"jest": "^21.2.1",
79
"prettier": "^0.21.0"
10+
},
11+
"jest": {
12+
"testPathIgnorePatterns": [
13+
"node_modules",
14+
"template"
15+
]
816
}
917
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// based on jest official doc: https://facebook.github.io/jest/docs/en/manual-mocks.html
2+
// __mocks__/fs.js
3+
'use strict';
4+
5+
const path = require('path');
6+
7+
const fs = jest.genMockFromModule('fs');
8+
9+
// This is a custom function that our tests can use during setup to specify
10+
// what the files on the "mock" filesystem should look like when any of the
11+
// `fs` APIs are used.
12+
let mockFiles = Object.create(null);
13+
function __setMockFiles(newMockFiles) {
14+
mockFiles = Object.create(null);
15+
for (const file in newMockFiles) {
16+
const dir = path.dirname(file);
17+
if (!mockFiles[dir]) {
18+
mockFiles[dir] = [];
19+
}
20+
mockFiles[dir].push(path.basename(file));
21+
}
22+
}
23+
24+
// A custom version of `readdirSync` that reads from the special mocked out
25+
// file list set via __setMockFiles
26+
function readdirSync(directoryPath) {
27+
return mockFiles[directoryPath] || [];
28+
}
29+
30+
function accessSync(directoryPath) {
31+
const dir = path.dirname(directoryPath);
32+
const children = mockFiles[dir];
33+
34+
if (!children || children.indexOf(path.basename(directoryPath)) < 0) {
35+
throw new TypeError(`no such file/dir: ${directoryPath}`);
36+
}
37+
}
38+
39+
fs.__setMockFiles = __setMockFiles;
40+
fs.readdirSync = readdirSync;
41+
fs.accessSync = accessSync;
42+
43+
module.exports = fs;

react-native-scripts/src/scripts/eject.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import path from 'path';
88
import rimraf from 'rimraf';
99
import spawn from 'cross-spawn';
1010
import log from '../util/log';
11+
import { hasYarn } from '../util/pm';
1112

1213
import { detach } from '../util/expo';
1314

@@ -78,7 +79,7 @@ Ejecting is permanent! Please be careful with your selection.
7879
const { ejectMethod } = await inquirer.prompt(questions);
7980

8081
if (ejectMethod === 'raw') {
81-
const useYarn = await fse.exists(path.resolve('yarn.lock'));
82+
const useYarn = hasYarn(process.cwd());
8283
const npmOrYarn = useYarn ? 'yarn' : 'npm';
8384
const appJson = JSON.parse(await fse.readFile(path.resolve('app.json')));
8485
const pkgJson = JSON.parse(await fse.readFile(path.resolve('package.json')));

react-native-scripts/src/scripts/init.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import pathExists from 'path-exists';
77
import spawn from 'cross-spawn';
88
import log from '../util/log';
99
import install from '../util/install';
10+
import { hasYarn } from '../util/pm';
1011

1112
// UPDATE DEPENDENCY VERSIONS HERE
1213
const DEFAULT_DEPENDENCIES = {
@@ -24,7 +25,7 @@ const DEFAULT_DEV_DEPENDENCIES = {
2425
module.exports = async (appPath: string, appName: string, verbose: boolean, cwd: string = '') => {
2526
const ownPackageName: string = require('../../package.json').name;
2627
const ownPath: string = path.join(appPath, 'node_modules', ownPackageName);
27-
const useYarn: boolean = await pathExists(path.join(appPath, 'yarn.lock'));
28+
const useYarn: boolean = hasYarn(appPath);
2829
const npmOrYarn = useYarn ? 'yarn' : 'npm';
2930

3031
// FIXME(perry) remove when npm 5 is supported

react-native-scripts/src/scripts/ios.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ import path from 'path';
88
import pathExists from 'path-exists';
99
import qr from 'qrcode-terminal';
1010
import log from '../util/log';
11+
import { hasYarn } from '../util/pm';
1112

1213
import packager from '../util/packager';
1314

1415
Config.validation.reactNativeVersionWarnings = false;
1516
Config.developerTool = 'crna';
1617
Config.offline = true;
1718

18-
const command: string = pathExists.sync(path.join(process.cwd(), 'yarn.lock')) ? 'yarnpkg' : 'npm';
19+
const command: string = hasYarn(process.cwd()) ? 'yarnpkg' : 'npm';
1920

2021
if (!Simulator.isPlatformSupported()) {
2122
log(
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict';
2+
3+
jest.mock('fs');
4+
5+
import { hasYarn } from '../pm';
6+
7+
describe('hasYarn', () => {
8+
const MOCK_FILE_INFO = {
9+
'/a/b/yarn.lock': 'fake yarn.lock',
10+
};
11+
12+
beforeEach(() => {
13+
// Set up some mocked out file info before each test
14+
require('fs').__setMockFiles(MOCK_FILE_INFO);
15+
});
16+
test('undefined path will throw exception', () => {
17+
expect(() => hasYarn(undefined, false)).toThrow();
18+
});
19+
test('empty path is ok', () => {
20+
expect(hasYarn('', false)).toEqual(false);
21+
});
22+
test('can find yarn in the given path', () => {
23+
expect(hasYarn('/a/b', false)).toEqual(true);
24+
});
25+
test('can find yarn in the parent path', () => {
26+
expect(hasYarn('/a/b/c/d/e', false)).toEqual(true);
27+
});
28+
test('can NOT find yarn in the children path', () => {
29+
expect(hasYarn('/a', false)).toEqual(false);
30+
});
31+
test('can use cached value 2nd time around', () => {
32+
expect(hasYarn('/a/b/c/d/e', false)).toEqual(true);
33+
expect(hasYarn(undefined, true)).toEqual(true);
34+
});
35+
});

react-native-scripts/src/util/install.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import spawn from 'cross-spawn';
44
import pathExists from 'path-exists';
55
import path from 'path';
66
import log from '../util/log';
7+
import { hasYarn } from './pm';
78

89
type InstallResult = {
910
code: number,
@@ -17,7 +18,7 @@ export default (async function install(
1718
packageVersion?: string,
1819
options?: any = {}
1920
): Promise<InstallResult> {
20-
const useYarn: boolean = await pathExists(path.join(appPath, 'yarn.lock'));
21+
const useYarn: boolean = hasYarn(appPath);
2122

2223
let command = '';
2324
let args = [];
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// @flow
2+
3+
'use strict';
4+
5+
import path from 'path';
6+
import fs from 'fs';
7+
8+
/**
9+
* check if the current path uses yarn, i.e. looking for yarn.lock in
10+
* the current path and up
11+
*
12+
* @param {*} startingPath a path where we will look for yarn.lock file.
13+
* Will traverse up the filesystem until we either find the file or reach the root
14+
*
15+
* @param {boolean} useCached if true and we have a cached hasYarn result, it will be returned, otherwise go through the
16+
* normal lookup logic described above. mainly for optimization purpose, default is true.
17+
*/
18+
let _hasYarn: ?boolean;
19+
export function hasYarn(startingPath: string, useCached: boolean = true): boolean {
20+
if (_hasYarn != null && useCached) {
21+
return _hasYarn;
22+
}
23+
24+
_hasYarn = false;
25+
let p = path.normalize(startingPath);
26+
while (p.length > 0) {
27+
const yarnLock = path.resolve(p, 'yarn.lock');
28+
try {
29+
const file = path.join(p, 'yarn.lock');
30+
fs.accessSync(file);
31+
_hasYarn = true;
32+
break;
33+
} catch (e) {
34+
const parsed = path.parse(p);
35+
if (parsed.root === parsed.dir) {
36+
break;
37+
}
38+
p = parsed.dir;
39+
}
40+
}
41+
return _hasYarn;
42+
}

react-native-scripts/taskfile.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export default async function (fly) {
1111
}
1212

1313
export async function babel(fly, opts) {
14-
await fly.source(opts.src || paths.source).babel().target(paths.build);
14+
await fly.source(opts.src || paths.source, {ignore: '**/__tests__/**'}).babel().target(paths.build);
1515
}
1616

1717
export async function clean(fly) {

0 commit comments

Comments
 (0)