Skip to content

Commit 765b3cb

Browse files
committed
[Fix] sync/async: fix preserveSymlinks option
Fixes #177.
1 parent f839d20 commit 765b3cb

File tree

15 files changed

+171
-46
lines changed

15 files changed

+171
-46
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# gitignore
22
node_modules
3+
**/node_modules
34

45
# Only apps should have lockfiles
56
npm-shrinkwrap.json

.travis.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ matrix:
3535
include:
3636
- node_js: "lts/*"
3737
env: PRETEST=true
38+
- node_js: "lts/*"
39+
env: POSTTEST=true
3840
- node_js: "11.3"
3941
env: TEST=true ALLOW_FAILURE=true
4042
- node_js: "11.2"

lib/async.js

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -40,22 +40,37 @@ module.exports = function resolve(x, options, callback) {
4040

4141
opts.paths = opts.paths || [];
4242

43-
if ((/^(?:\.\.?(?:\/|$)|\/|([A-Za-z]:)?[/\\])/).test(x)) {
44-
var res = path.resolve(basedir, x);
45-
if (x === '..' || x.slice(-1) === '/') res += '/';
46-
if ((/\/$/).test(x) && res === basedir) {
47-
loadAsDirectory(res, opts.package, onfile);
48-
} else loadAsFile(res, opts.package, onfile);
49-
} else loadNodeModules(x, basedir, function (err, n, pkg) {
50-
if (err) cb(err);
51-
else if (n) cb(null, n, pkg);
52-
else if (core[x]) return cb(null, x);
53-
else {
54-
var moduleError = new Error("Cannot find module '" + x + "' from '" + parent + "'");
55-
moduleError.code = 'MODULE_NOT_FOUND';
56-
cb(moduleError);
57-
}
58-
});
43+
// ensure that `basedir` is an absolute path at this point, resolving against the process' current working directory
44+
var absoluteStart = path.resolve(basedir);
45+
46+
if (opts.preserveSymlinks === false) {
47+
fs.realpath(absoluteStart, function (realPathErr, realStart) {
48+
if (realPathErr && realPathErr.code !== 'ENOENT') cb(err);
49+
else init(realStart);
50+
});
51+
} else {
52+
init(absoluteStart);
53+
}
54+
55+
var res;
56+
function init(basedir) {
57+
if ((/^(?:\.\.?(?:\/|$)|\/|([A-Za-z]:)?[/\\])/).test(x)) {
58+
res = path.resolve(basedir, x);
59+
if (x === '..' || x.slice(-1) === '/') res += '/';
60+
if ((/\/$/).test(x) && res === basedir) {
61+
loadAsDirectory(res, opts.package, onfile);
62+
} else loadAsFile(res, opts.package, onfile);
63+
} else loadNodeModules(x, basedir, function (err, n, pkg) {
64+
if (err) cb(err);
65+
else if (n) cb(null, n, pkg);
66+
else if (core[x]) return cb(null, x);
67+
else {
68+
var moduleError = new Error("Cannot find module '" + x + "' from '" + parent + "'");
69+
moduleError.code = 'MODULE_NOT_FOUND';
70+
cb(moduleError);
71+
}
72+
});
73+
}
5974

6075
function onfile(err, m, pkg) {
6176
if (err) cb(err);

lib/node-modules-paths.js

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
var path = require('path');
2-
var fs = require('fs');
32
var parse = path.parse || require('path-parse');
43

54
var getNodeModulesDirs = function getNodeModulesDirs(absoluteStart, modules) {
@@ -29,28 +28,15 @@ module.exports = function nodeModulesPaths(start, opts, request) {
2928
? [].concat(opts.moduleDirectory)
3029
: ['node_modules'];
3130

32-
// ensure that `start` is an absolute path at this point, resolving against the process' current working directory
33-
var absoluteStart = path.resolve(start);
34-
35-
if (opts && opts.preserveSymlinks === false) {
36-
try {
37-
absoluteStart = fs.realpathSync(absoluteStart);
38-
} catch (err) {
39-
if (err.code !== 'ENOENT') {
40-
throw err;
41-
}
42-
}
43-
}
44-
4531
if (opts && typeof opts.paths === 'function') {
4632
return opts.paths(
4733
request,
48-
absoluteStart,
49-
function () { return getNodeModulesDirs(absoluteStart, modules); },
34+
start,
35+
function () { return getNodeModulesDirs(start, modules); },
5036
opts
5137
);
5238
}
5339

54-
var dirs = getNodeModulesDirs(absoluteStart, modules);
40+
var dirs = getNodeModulesDirs(start, modules);
5541
return opts && opts.paths ? dirs.concat(opts.paths) : dirs;
5642
};

lib/sync.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,26 @@ module.exports = function (x, options) {
3030

3131
opts.paths = opts.paths || [];
3232

33+
// ensure that `basedir` is an absolute path at this point, resolving against the process' current working directory
34+
var absoluteStart = path.resolve(basedir);
35+
36+
if (opts.preserveSymlinks === false) {
37+
try {
38+
absoluteStart = fs.realpathSync(absoluteStart);
39+
} catch (realPathErr) {
40+
if (realPathErr.code !== 'ENOENT') {
41+
throw realPathErr;
42+
}
43+
}
44+
}
45+
3346
if ((/^(?:\.\.?(?:\/|$)|\/|([A-Za-z]:)?[/\\])/).test(x)) {
34-
var res = path.resolve(basedir, x);
47+
var res = path.resolve(absoluteStart, x);
3548
if (x === '..' || x.slice(-1) === '/') res += '/';
3649
var m = loadAsFileSync(res) || loadAsDirectorySync(res);
3750
if (m) return m;
3851
} else {
39-
var n = loadNodeModulesSync(x, basedir);
52+
var n = loadNodeModulesSync(x, absoluteStart);
4053
if (n) return n;
4154
}
4255

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
"lint": "eslint .",
1919
"tests-only": "tape test/*.js",
2020
"pretest": "npm run lint",
21-
"test": "npm run --silent tests-only"
21+
"test": "npm run --silent tests-only",
22+
"posttest": "npm run test:multirepo",
23+
"test:multirepo": "cd ./test/resolver/multirepo && npm install && npm test"
2224
},
2325
"devDependencies": {
2426
"@ljharb/eslint-config": "^13.0.0",

test/node_path.js

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,31 @@
1+
var fs = require('fs');
12
var path = require('path');
23
var test = require('tape');
34
var resolve = require('../');
45

56
test('$NODE_PATH', function (t) {
67
t.plan(8);
78

9+
var isDir = function (dir, cb) {
10+
if (dir === '/node_path' || dir === 'node_path/x') {
11+
return cb(null, true);
12+
}
13+
fs.stat(dir, function (err, stat) {
14+
if (!err) {
15+
return cb(null, stat.isDirectory());
16+
}
17+
if (err.code === 'ENOENT' || err.code === 'ENOTDIR') return cb(null, false);
18+
return cb(err);
19+
});
20+
};
21+
822
resolve('aaa', {
923
paths: [
1024
path.join(__dirname, '/node_path/x'),
1125
path.join(__dirname, '/node_path/y')
1226
],
13-
basedir: __dirname
27+
basedir: __dirname,
28+
isDirectory: isDir
1429
}, function (err, res) {
1530
t.error(err);
1631
t.equal(res, path.join(__dirname, '/node_path/x/aaa/index.js'), 'aaa resolves');
@@ -21,7 +36,8 @@ test('$NODE_PATH', function (t) {
2136
path.join(__dirname, '/node_path/x'),
2237
path.join(__dirname, '/node_path/y')
2338
],
24-
basedir: __dirname
39+
basedir: __dirname,
40+
isDirectory: isDir
2541
}, function (err, res) {
2642
t.error(err);
2743
t.equal(res, path.join(__dirname, '/node_path/y/bbb/index.js'), 'bbb resolves');
@@ -32,21 +48,20 @@ test('$NODE_PATH', function (t) {
3248
path.join(__dirname, '/node_path/x'),
3349
path.join(__dirname, '/node_path/y')
3450
],
35-
basedir: __dirname
51+
basedir: __dirname,
52+
isDirectory: isDir
3653
}, function (err, res) {
3754
t.error(err);
3855
t.equal(res, path.join(__dirname, '/node_path/x/ccc/index.js'), 'ccc resolves');
3956
});
4057

41-
/*
42-
* ensure that relative paths still resolve against the
43-
* regular `node_modules` correctly
44-
*/
58+
// ensure that relative paths still resolve against the regular `node_modules` correctly
4559
resolve('tap', {
4660
paths: [
4761
'node_path'
4862
],
49-
basedir: 'node_path/x'
63+
basedir: path.join(__dirname, 'node_path/x'),
64+
isDirectory: isDir
5065
}, function (err, res) {
5166
var root = require('tap/package.json').main;
5267
t.error(err);

test/resolver.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,15 +364,16 @@ test('async dot slash main', function (t) {
364364
});
365365

366366
test('not a directory', function (t) {
367-
t.plan(5);
367+
t.plan(6);
368368
var path = './foo';
369369
resolve(path, { basedir: __filename }, function (err, res, pkg) {
370370
t.ok(err, 'a non-directory errors');
371371
t.equal(arguments.length, 1);
372372
t.equal(res, undefined);
373373
t.equal(pkg, undefined);
374374

375-
t.equal(err && err.message, 'Cannot find module \'' + path + "' from '" + __filename + "'");
375+
t.equal(err && err.message, 'Cannot find module \'' + path + '\' from \'' + __filename + '\'');
376+
t.equal(err && err.code, 'MODULE_NOT_FOUND');
376377
});
377378
});
378379

test/resolver/multirepo/.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package-lock=false

test/resolver/multirepo/lerna.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"packages": [
3+
"packages/*"
4+
],
5+
"version": "0.0.0"
6+
}

test/resolver/multirepo/package.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "monorepo-symlink-test",
3+
"private": true,
4+
"version": "0.0.0",
5+
"description": "",
6+
"main": "index.js",
7+
"scripts": {
8+
"postinstall": "lerna bootstrap",
9+
"test": "node packages/package-a"
10+
},
11+
"author": "",
12+
"license": "MIT",
13+
"dependencies": {
14+
"jquery": "^3.3.1",
15+
"resolve": "../../../"
16+
},
17+
"devDependencies": {
18+
"lerna": "^3.4.3"
19+
}
20+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict';
2+
3+
var assert = require('assert');
4+
var path = require('path');
5+
var resolve = require('resolve');
6+
7+
var basedir = __dirname + '/node_modules/@my-scope/package-b';
8+
9+
var expected = path.join(__dirname, '../../node_modules/jquery/dist/jquery.js');
10+
11+
/*
12+
* preserveSymlinks === false
13+
* will search NPM package from
14+
* - packages/package-b/node_modules
15+
* - packages/node_modules
16+
* - node_modules
17+
*/
18+
assert.equal(resolve.sync('jquery', { basedir: basedir, preserveSymlinks: false }), expected);
19+
assert.equal(resolve.sync('../../node_modules/jquery', { basedir: basedir, preserveSymlinks: false }), expected);
20+
21+
/*
22+
* preserveSymlinks === true
23+
* will search NPM package from
24+
* - packages/package-a/node_modules/@my-scope/packages/package-b/node_modules
25+
* - packages/package-a/node_modules/@my-scope/packages/node_modules
26+
* - packages/package-a/node_modules/@my-scope/node_modules
27+
* - packages/package-a/node_modules/node_modules
28+
* - packages/package-a/node_modules
29+
* - packages/node_modules
30+
* - node_modules
31+
*/
32+
assert.equal(resolve.sync('jquery', { basedir: basedir, preserveSymlinks: true }), expected);
33+
assert.equal(resolve.sync('../../../../../node_modules/jquery', { basedir: basedir, preserveSymlinks: true }), expected);
34+
35+
console.log(' * all monorepo paths successfully resolved through symlinks');
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "@my-scope/package-a",
3+
"version": "0.0.0",
4+
"private": true,
5+
"description": "",
6+
"license": "MIT",
7+
"main": "index.js",
8+
"scripts": {
9+
"test": "echo \"Error: run tests from root\" && exit 1"
10+
},
11+
"dependencies": {
12+
"@my-scope/package-b": "^0.0.0"
13+
}
14+
}

test/resolver/multirepo/packages/package-b/index.js

Whitespace-only changes.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "@my-scope/package-b",
3+
"private": true,
4+
"version": "0.0.0",
5+
"description": "",
6+
"license": "MIT",
7+
"main": "index.js",
8+
"scripts": {
9+
"test": "echo \"Error: run tests from root\" && exit 1"
10+
},
11+
"dependencies": {
12+
"@my-scope/package-a": "^0.0.0"
13+
}
14+
}

0 commit comments

Comments
 (0)