Skip to content

Commit

Permalink
Find all symlinks in (scoped) node_modules and merge root flag
Browse files Browse the repository at this point in the history
  • Loading branch information
Swaagie committed Feb 16, 2017
1 parent ae57b25 commit db6f5f4
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 24 deletions.
6 changes: 5 additions & 1 deletion local-cli/server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@

const chalk = require('chalk');
const formatBanner = require('./formatBanner');
const findSymlinksPaths = require('../util/findSymlinksPaths');
const path = require('path');
const runServer = require('./runServer');

/**
* Starts the React Native Packager Server.
*/
function server(argv, config, args) {
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
), []));

console.log(formatBanner(
'Running packager on port ' + args.port + '.\n\n' +
Expand Down
82 changes: 82 additions & 0 deletions local-cli/util/__tests__/findSymlinksPaths.spec.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
64 changes: 41 additions & 23 deletions local-cli/util/findSymlinksPaths.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
8 changes: 8 additions & 0 deletions packager/src/node-haste/DependencyGraph/ResolutionRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 isAbsolutePath = require('absolute-path');
Expand Down Expand Up @@ -441,6 +442,13 @@ class ResolutionRequest {

_loadAsDir(potentialDirPath, fromModule, toModule) {
return Promise.resolve().then(() => {
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,
Expand Down

0 comments on commit db6f5f4

Please sign in to comment.