diff --git a/local-cli/server/server.js b/local-cli/server/server.js index 4825fc9a02bccb..41c1aea7d491ee 100644 --- a/local-cli/server/server.js +++ b/local-cli/server/server.js @@ -10,12 +10,16 @@ const path = require('path'); const runServer = require('./runServer'); +const findSymlinksPaths = require('../util/findSymlinksPaths'); /** * Starts the React Native Packager Server. */ function server(argv, config, args) { - args.projectRoots = args.projectRoots.concat(args.root); + Array.prototype.push.apply(args.projectRoots, args.root.reduce((roots, src) => roots.concat( + findSymlinksPaths(path.join(src, 'node_modules'), args.projectRoots), + src + ), [])); const startedCallback = logReporter => { logReporter.update({ diff --git a/local-cli/util/__tests__/findSymlinksPaths.spec.js b/local-cli/util/__tests__/findSymlinksPaths.spec.js new file mode 100644 index 00000000000000..9ec6c5a1c3e580 --- /dev/null +++ b/local-cli/util/__tests__/findSymlinksPaths.spec.js @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +'use strict'; + +jest.autoMockOff(); + +const log = require('npmlog'); +const fs = require('fs'); + +describe('Utils', () => { + beforeEach(() => { + jest.resetModules(); + delete require.cache[require.resolve('../findSymlinksPaths')]; + log.level = 'silent'; + }); + + it('should read scoped packages', () => { + const readdirSync = jest.fn() + .mockImplementationOnce(() => [ 'module1', 'module2', '@jest', '@react' ]) + .mockImplementationOnce(() => [ 'mocks', 'tests' ]) + .mockImplementationOnce(() => [ 'native' ]); + const lstatSync = jest.fn((str) => new fs.Stats()) + + const mock = jest.setMock('fs', { + readdirSync, + lstatSync + }); + + const find = require('../findSymlinksPaths'); + const links = find(__dirname, []); + + expect(readdirSync.mock.calls.length).toEqual(3); + expect(readdirSync.mock.calls[1][0]).toContain('__tests__/@jest'); + expect(readdirSync.mock.calls[2][0]).toContain('__tests__/@react'); + + expect(lstatSync.mock.calls.length).toEqual(7); + expect(lstatSync.mock.calls[2][0]).toContain('__tests__/@jest/mocks'); + expect(lstatSync.mock.calls[3][0]).toContain('__tests__/@jest/tests'); + expect(lstatSync.mock.calls[5][0]).toContain('__tests__/@react/native'); + expect(links.length).toEqual(0); + }); + + it('should attempt to read symlinks node_modules folder for nested symlinks', function () { + const link = new fs.Stats(16777220, 41453); + const dir = new fs.Stats(16777220, 16877); + + const readdirSync = jest.fn() + .mockImplementationOnce(() => [ 'symlink' ]) + .mockImplementationOnce(() => [ 'deeperLink' ]) + const lstatSync = jest.fn() + .mockImplementationOnce(str => link) + .mockImplementationOnce(str => dir) // shortcircuits while loop + .mockImplementationOnce(str => dir) + .mockImplementationOnce(str => link) + .mockImplementationOnce(str => dir) // shortcircuits while loop + .mockImplementationOnce(str => new fs.Stats()); + + const mock = jest.setMock('fs', { + readlinkSync: str => str, + existsSync: () => true, + readdirSync, + lstatSync + }); + + const find = require('../findSymlinksPaths'); + const links = find(__dirname, []); + + expect(links.length).toEqual(2); + + expect(lstatSync.mock.calls[0][0]).toContain('__tests__/symlink'); + expect(lstatSync.mock.calls[2][0]).toContain('__tests__/symlink/node_modules'); + expect(lstatSync.mock.calls[3][0]).toContain('__tests__/symlink/node_modules/deeperLink'); + expect(lstatSync.mock.calls[5][0]).toContain('__tests__/symlink/node_modules/deeperLink/node_modules'); + }); +}); diff --git a/local-cli/util/findSymlinksPaths.js b/local-cli/util/findSymlinksPaths.js index 8dc7d081ecb8ed..0608fb7d115869 100644 --- a/local-cli/util/findSymlinksPaths.js +++ b/local-cli/util/findSymlinksPaths.js @@ -7,36 +7,54 @@ const fs = require('fs'); */ module.exports = function findSymlinksPaths(lookupFolder, ignoredRoots) { const timeStart = Date.now(); - const folders = fs.readdirSync(lookupFolder); - const resolvedSymlinks = []; - folders.forEach(folder => { - const visited = []; - - let symlink = path.resolve(lookupFolder, folder); - while (fs.lstatSync(symlink).isSymbolicLink()) { - const index = visited.indexOf(symlink); - if (index !== -1) { - throw Error( - `Infinite symlink recursion detected:\n ` + - visited.slice(index).join(`\n `) + let n = 0; + + function findSymLinks(base) { + const folders = fs.readdirSync(base); + n += folders.length; + + folders.forEach(folder => { + const visited = []; + let symlink = path.resolve(base, folder); + + // Resolve symlinks from scoped modules. + if (path.basename(symlink).charAt(0) === '@') { + findSymLinks(symlink); + } + + while (fs.lstatSync(symlink).isSymbolicLink()) { + const index = visited.indexOf(symlink); + if (index !== -1) { + throw Error( + `Infinite symlink recursion detected:\n ` + + visited.slice(index).join(`\n `) + ); + } + + visited.push(symlink); + symlink = path.resolve( + path.dirname(symlink), + fs.readlinkSync(symlink) ); } - visited.push(symlink); - symlink = path.resolve( - path.dirname(symlink), - fs.readlinkSync(symlink) - ); - } + if (visited.length && !rootExists(ignoredRoots, symlink)) { + resolvedSymlinks.push(symlink); + + // Also find symlinks from symlinked lookupFolder. + const modules = path.join(symlink, 'node_modules'); + if (fs.existsSync(modules) && fs.lstatSync(modules).isDirectory()) { + findSymLinks(modules); + } + } + }); + } - if (visited.length && !rootExists(ignoredRoots, symlink)) { - resolvedSymlinks.push(symlink); - } - }); + findSymLinks(lookupFolder); const timeEnd = Date.now(); - console.log(`Scanning ${folders.length} folders for symlinks in ${lookupFolder} (${timeEnd - timeStart}ms)`); + console.log(`Scanning ${n} folders for symlinks in ${lookupFolder} (${timeEnd - timeStart}ms)`); return resolvedSymlinks; }; diff --git a/packager/src/node-haste/DependencyGraph/ResolutionRequest.js b/packager/src/node-haste/DependencyGraph/ResolutionRequest.js index 77ec18e097832a..3772c18bb74dd5 100644 --- a/packager/src/node-haste/DependencyGraph/ResolutionRequest.js +++ b/packager/src/node-haste/DependencyGraph/ResolutionRequest.js @@ -15,6 +15,7 @@ const MapWithDefaults = require('../lib/MapWithDefaults'); const debug = require('debug')('RNP:DependencyGraph'); const util = require('util'); +const fs = require('fs'); const path = require('path'); const realPath = require('path'); const invariant = require('fbjs/lib/invariant'); @@ -499,6 +500,13 @@ class ResolutionRequest { } _loadAsDir(potentialDirPath: string, fromModule: TModule, toModule: string): TModule { + while (fs.existsSync(potentialDirPath) && fs.lstatSync(potentialDirPath).isSymbolicLink()) { + potentialDirPath = path.resolve( + path.dirname(potentialDirPath), + fs.readlinkSync(potentialDirPath) + ); + } + if (!this._dirExists(potentialDirPath)) { throw new UnableToResolveError( fromModule,