Skip to content

Commit 45772af

Browse files
committed
install: do not descend into directory deps' child modules
This builds on the work of #217, bringing back the logic in 2f0c883 for all deps other than 'directory' symlinks where it causes problems. This also causes the installer to *fix* shrinkwraps that inappropriately list the dependencies of directory symlink packages, which goes further to address the problems highlighted in https://npm.community/t/installing-the-same-module-under-multiple-relative-paths-fails-on-linux/8863 and https://npm.community/t/reinstall-breaks-after-npm-update-to-6-10-2/9327, even if a prior npm install created a broken package-lock.json file. Related: #217 Credit: @isaacs Fix: https://npm.community/t/reinstall-breaks-after-npm-update-to-6-10-2/9327 Fix: https://npm.community/t/installing-the-same-module-under-multiple-relative-paths-fails-on-linux/8863
1 parent 24acc9f commit 45772af

File tree

3 files changed

+146
-7
lines changed

3 files changed

+146
-7
lines changed

lib/install/inflate-shrinkwrap.js

+14-6
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,12 @@ function inflateShrinkwrap (topPath, tree, swdeps, opts) {
5050

5151
return BB.each(Object.keys(swdeps), (name) => {
5252
const sw = swdeps[name]
53-
const dependencies = sw.dependencies || {}
5453
const requested = realizeShrinkwrapSpecifier(name, sw, topPath)
54+
// We should not muck about in the node_modules folder of
55+
// symlinked packages. Treat its dependencies as if they
56+
// were empty, since it's none of our business.
57+
const dependencies = requested.type === 'directory' ? {}
58+
: sw.dependencies || {}
5559
return inflatableChild(
5660
onDisk[name], name, topPath, tree, sw, requested, opts
5761
).then((child) => {
@@ -141,6 +145,10 @@ function isGit (sw) {
141145
}
142146

143147
function makeFakeChild (name, topPath, tree, sw, requested) {
148+
// We should not muck about in the node_modules folder of
149+
// symlinked packages. Treat its dependencies as if they
150+
// were empty, since it's none of our business.
151+
const isDirectory = requested.type === 'directory'
144152
const from = sw.from || requested.raw
145153
const pkg = {
146154
name: name,
@@ -156,7 +164,7 @@ function makeFakeChild (name, topPath, tree, sw, requested) {
156164
_spec: requested.rawSpec,
157165
_where: topPath,
158166
_args: [[requested.toString(), topPath]],
159-
dependencies: sw.requires
167+
dependencies: isDirectory ? {} : sw.requires
160168
}
161169

162170
if (!sw.bundled) {
@@ -167,16 +175,16 @@ function makeFakeChild (name, topPath, tree, sw, requested) {
167175
}
168176
const child = createChild({
169177
package: pkg,
170-
loaded: true,
178+
loaded: isDirectory,
171179
parent: tree,
172180
children: [],
173181
fromShrinkwrap: requested,
174182
fakeChild: sw,
175183
fromBundle: sw.bundled ? tree.fromBundle || tree : null,
176184
path: childPath(tree.path, pkg),
177-
realpath: requested.type === 'directory' ? requested.fetchSpec : childPath(tree.realpath, pkg),
185+
realpath: isDirectory ? requested.fetchSpec : childPath(tree.realpath, pkg),
178186
location: (tree.location === '/' ? '' : tree.location + '/') + pkg.name,
179-
isLink: requested.type === 'directory',
187+
isLink: isDirectory,
180188
isInLink: tree.isLink || tree.isInLink,
181189
swRequires: sw.requires
182190
})
@@ -195,7 +203,7 @@ function fetchChild (topPath, tree, sw, requested) {
195203
var isLink = pkg._requested.type === 'directory'
196204
const child = createChild({
197205
package: pkg,
198-
loaded: false,
206+
loaded: isLink,
199207
parent: tree,
200208
fromShrinkwrap: requested,
201209
path: childPath(tree.path, pkg),

test/tap/install-from-local-multipath.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,13 @@ test('\'npm install\' should install local packages', function (t) {
167167
'install', '.'
168168
],
169169
EXEC_OPTS,
170-
function (err, code) {
170+
function (err, code, stdout, stderr) {
171171
t.ifError(err, 'error should not exist')
172172
t.notOk(code, 'npm install exited with code 0')
173+
// if the test fails, let's see why
174+
if (err || code) {
175+
console.error({code, stdout, stderr})
176+
}
173177
t.end()
174178
}
175179
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
const common = require('../common-tap.js')
2+
const Tacks = require('tacks')
3+
const {File, Dir} = Tacks
4+
const pkg = common.pkg
5+
const t = require('tap')
6+
7+
// via https://github.com/chhetrisushil/npm-update-test
8+
const goodPackage2Entry = {
9+
version: 'file:../package2',
10+
dev: true
11+
}
12+
13+
const badPackage2Entry = {
14+
version: 'file:../package2',
15+
dev: true,
16+
dependencies: {
17+
'@test/package3': {
18+
version: '1.0.0'
19+
}
20+
}
21+
}
22+
23+
const goodPackage1Deps = {
24+
'@test/package2': goodPackage2Entry,
25+
'@test/package3': {
26+
version: 'file:../package3',
27+
dev: true
28+
}
29+
}
30+
31+
const badPackage1Deps = {
32+
'@test/package2': badPackage2Entry,
33+
'@test/package3': {
34+
version: 'file:../package3',
35+
dev: true
36+
}
37+
}
38+
39+
const badPackage1Lock = {
40+
name: 'package1',
41+
version: '1.0.0',
42+
lockfileVersion: 1,
43+
requires: true,
44+
dependencies: badPackage1Deps
45+
}
46+
47+
const goodPackage1Lock = {
48+
name: 'package1',
49+
version: '1.0.0',
50+
lockfileVersion: 1,
51+
requires: true,
52+
dependencies: goodPackage1Deps
53+
}
54+
55+
const package2Lock = {
56+
name: 'package2',
57+
version: '1.0.0',
58+
lockfileVersion: 1,
59+
requires: true,
60+
dependencies: {
61+
'@test/package3': {
62+
version: 'file:../package3',
63+
dev: true
64+
}
65+
}
66+
}
67+
68+
const package3Lock = {
69+
name: 'package3',
70+
version: '1.0.0',
71+
lockfileVersion: 1
72+
}
73+
74+
t.test('setup fixture', t => {
75+
const fixture = new Tacks(new Dir({
76+
package1: new Dir({
77+
'package-lock.json': new File(badPackage1Lock),
78+
'package.json': new File({
79+
name: 'package1',
80+
version: '1.0.0',
81+
devDependencies: {
82+
'@test/package2': 'file:../package2',
83+
'@test/package3': 'file:../package3'
84+
}
85+
})
86+
}),
87+
package2: new Dir({
88+
'package-lock.json': new File(package2Lock),
89+
'package.json': new File({
90+
name: 'package2',
91+
version: '1.0.0',
92+
devDependencies: {
93+
'@test/package3': 'file:../package3'
94+
}
95+
})
96+
}),
97+
package3: new Dir({
98+
'package-lock.json': new File(package3Lock),
99+
'package.json': new File({
100+
name: 'package3',
101+
version: '1.0.0'
102+
})
103+
})
104+
}))
105+
fixture.create(pkg)
106+
t.end()
107+
})
108+
109+
t.test('install does not error', t =>
110+
common.npm(['install', '--no-registry'], { cwd: pkg + '/package1' })
111+
.then(([code, out, err]) => {
112+
t.equal(code, 0)
113+
console.error({out, err})
114+
}))
115+
116+
t.test('updated package-lock.json to good state, left others alone', t => {
117+
const fs = require('fs')
118+
const getlock = n => {
119+
const file = pkg + '/package' + n + '/package-lock.json'
120+
const lockdata = fs.readFileSync(file, 'utf8')
121+
return JSON.parse(lockdata)
122+
}
123+
t.strictSame(getlock(1), goodPackage1Lock)
124+
t.strictSame(getlock(2), package2Lock)
125+
t.strictSame(getlock(3), package3Lock)
126+
t.end()
127+
})

0 commit comments

Comments
 (0)