Skip to content
This repository has been archived by the owner on Aug 17, 2019. It is now read-only.

Commit

Permalink
feat(optional): ignore failed optional deps
Browse files Browse the repository at this point in the history
Fixes: #3
  • Loading branch information
zkat committed Oct 11, 2017
1 parent 3b98fb3 commit 19e3a08
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 4 deletions.
50 changes: 50 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Installer {
return this.prepare()
.then(() => this.runScript('preinstall', this.pkg, this.prefix))
.then(() => this.extractTree(this.logicalTree))
.then(() => this.garbageCollect(this.logicalTree))
.then(() => this.runScript('install', this.pkg, this.prefix))
.then(() => this.runScript('postinstall', this.pkg, this.prefix))
.then(() => this.runScript('prepublish', this.pkg, this.prefix))
Expand Down Expand Up @@ -137,11 +138,58 @@ class Installer {
this.pkgCount++
return this
})
.catch(e => {
if (child.optional) {
this.pkgCount++
child.failed = true
return rimraf(childPath)
} else {
throw e
}
})
})
return child.pending
}, { concurrency: 50 })
}

// A cute little mark-and-sweep collector!
garbageCollect (tree) {
const liveDeps = new Set()
const installer = this
mark(tree)
return sweep(tree)

function mark (tree) {
for (let dep of tree.dependencies.values()) {
if (dep.seenByMark) { continue }
dep.seenByMark = true
if (!dep.optional || !dep.failed) {
liveDeps.add(dep)
mark(dep)
}
}
}

function sweep (tree) {
return BB.map(tree.dependencies.values(), dep => {
if (dep.seenBySweep) { return }
dep.seenBySweep = true
return sweep(dep).then(() => {
if (!liveDeps.has(dep) && !dep.purged) {
const depPath = path.join(
installer.prefix,
'node_modules',
dep.address.replace(/:/g, '/node_modules/')
)
installer.pkgCount--
dep.purged = true
return rimraf(depPath)
}
})
}, { concurrency: 100 })
}
}

runScript (stage, pkg, pkgPath) {
if (!this.config.lifecycleOpts.ignoreScripts && pkg.scripts && pkg.scripts[stage]) {
// TODO(mikesherov): remove pkg._id when npm-lifecycle no longer relies on it
Expand All @@ -151,7 +199,9 @@ class Installer {
return BB.resolve()
}
}

module.exports = Installer

module.exports._readJson = readJson

function readJson (jsonPath, name, ignoreMissing) {
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"graceful-fs": "^4.1.11",
"lock-verify": "^1.1.0",
"npm-lifecycle": "^1.0.3",
"npm-logical-tree": "^1.0.0",
"npm-logical-tree": "^1.1.0",
"npm-package-arg": "^5.1.2",
"npmlog": "^4.1.2",
"pacote": "^6.0.4",
Expand Down
108 changes: 108 additions & 0 deletions test/specs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ test('handles dependency list with only shallow subdeps', t => {
}
}),
'package-lock.json': File({
name: pkgName,
verson: pkgVersion,
dependencies: {
a: {
version: '1.1.1'
Expand Down Expand Up @@ -262,6 +264,112 @@ test('prioritizes npm-shrinkwrap over package-lock if both present', t => {
})
})

test('removes optional dependencies', t => {
const fixture = new Tacks(Dir({
'package.json': File({
name: pkgName,
version: pkgVersion,
dependencies: {
a: '^1'
},
optionalDependencies: {
b: '^2'
}
}),
'package-lock.json': File({
lockfileVersion: 1,
requires: true,
dependencies: {
a: {
version: '1.0.0',
requires: {
b: '2.0.0',
d: '4.0.0'
}
},
b: {
version: '2.0.0',
optional: true,
requires: {
c: '3.0.0',
d: '4.0.0'
}
},
c: {
version: '3.0.0',
optional: true
},
d: {
version: '4.0.0'
}
}
})
}))
fixture.create(prefix)

extract = (name, child, childPath, opts) => {
let files
if (child.name === 'a') {
files = new Tacks(Dir({
'package.json': File({
name: 'a',
version: '1.0.0',
dependencies: {
b: '^2',
d: '^4'
}
})
}))
} else if (child.name === 'b') {
files = new Tacks(Dir({
'package.json': File({
name: 'b',
version: '2.0.0',
dependencies: {
c: '^3',
d: '^4'
},
scripts: {
install: 'exit 1'
}
})
}))
} else if (child.name === 'c') {
files = new Tacks(Dir({
'package.json': File({
name: 'c',
version: '3.0.0'
})
}))
} else if (child.name === 'd') {
files = new Tacks(Dir({
'package.json': File({
name: 'd',
version: '4.0.0'
})
}))
}
files.create(childPath)
}

const originalConsoleLog = console.log
console.log = () => {}
return new Installer({prefix}).run().then(details => {
console.log = originalConsoleLog
t.ok(true, 'installer succeeded even with optDep failure')
t.equal(details.pkgCount, 2, 'only successful deps counted')
const modP = path.join(prefix, 'node_modules')
t.ok(fs.statSync(path.join(modP, 'a')), 'dep a is there')
t.ok(fs.statSync(path.join(modP, 'd')), 'transitive dep d is there')
t.throws(() => {
fs.statSync(path.join(prefix, 'node_modules', 'b'))
}, 'failed optional dep b not in node_modules')
t.throws(() => {
fs.statSync(path.join(prefix, 'node_modules', 'c'))
}, 'isolated dependency d of failed dep removed')
})
})

test('runs lifecycle hooks of packages with env variables', t => {
const originalConsoleLog = console.log
console.log = () => {}
Expand Down

0 comments on commit 19e3a08

Please sign in to comment.