diff --git a/bin/lib/options.js b/bin/lib/options.js index bf8e08ec2..a1b671962 100644 --- a/bin/lib/options.js +++ b/bin/lib/options.js @@ -33,7 +33,13 @@ for (const arg of process.argv.slice(2)) { options.omit.push(arg.substr('--omit='.length)) } else if (/^--before=/.test(arg)) options.before = new Date(arg.substr('--before='.length)) - else if (/^--[^=]+=/.test(arg)) { + else if (/^-w.+/.test(arg)) { + options.workspaces = options.workspaces || [] + options.workspaces.push(arg.replace(/^-w/, '')) + } else if (/^--workspace=/.test(arg)) { + options.workspaces = options.workspaces || [] + options.workspaces.push(arg.replace(/^--workspace=/, '')) + } else if (/^--[^=]+=/.test(arg)) { const [key, ...v] = arg.replace(/^--/, '').split('=') const val = v.join('=') options[key] = val === 'false' ? false : val === 'true' ? true : val diff --git a/lib/arborist/build-ideal-tree.js b/lib/arborist/build-ideal-tree.js index f7e5b7e32..b8f4a6468 100644 --- a/lib/arborist/build-ideal-tree.js +++ b/lib/arborist/build-ideal-tree.js @@ -1480,9 +1480,15 @@ This is a one-time fix-up, please be patient... if (target.children.has(edge.name)) { const current = target.children.get(edge.name) - // same thing = keep - if (dep.matches(current)) - return KEEP + // same thing = keep, UNLESS the current doesn't satisfy and new + // one does satisfy. This can happen if it's a link to a matching target + // at a different location, which satisfies a version dep, but not a + // file: dep. If neither of them satisfy, then we can replace it, + // because presumably it's better for a peer or something. + if (dep.matches(current)) { + if (current.satisfies(edge) || !dep.satisfies(edge)) + return KEEP + } const { version: curVer } = current const { version: newVer } = dep diff --git a/lib/arborist/reify.js b/lib/arborist/reify.js index f0eae822a..e563f6257 100644 --- a/lib/arborist/reify.js +++ b/lib/arborist/reify.js @@ -27,6 +27,7 @@ const retirePath = require('../retire-path.js') const promiseAllRejectLate = require('promise-all-reject-late') const optionalSet = require('../optional-set.js') const updateRootPackageJson = require('../update-root-package-json.js') +const calcDepFlags = require('../calc-dep-flags.js') const _retiredPaths = Symbol('retiredPaths') const _retiredUnchanged = Symbol('retiredUnchanged') @@ -37,6 +38,8 @@ const _retireShallowNodes = Symbol.for('retireShallowNodes') const _getBundlesByDepth = Symbol('getBundlesByDepth') const _registryResolved = Symbol('registryResolved') const _addNodeToTrashList = Symbol('addNodeToTrashList') +const _workspaces = Symbol('workspaces') + // shared by rebuild mixin const _trashList = Symbol.for('trashList') const _handleOptionalFailure = Symbol.for('handleOptionalFailure') @@ -97,8 +100,10 @@ module.exports = cls => class Reifier extends cls { packageLockOnly = false, dryRun = false, formatPackageLock = true, + workspaces = [], } = options + this[_workspaces] = workspaces this[_dryRun] = !!dryRun this[_packageLockOnly] = !!packageLockOnly this[_savePrefix] = savePrefix @@ -270,9 +275,35 @@ module.exports = cls => class Reifier extends cls { // to just invalidate the parts that changed, but avoid walking the // whole tree again. + const filterNodes = [] + if (this[_global] && this[_explicitRequests].size) { + const idealTree = this.idealTree.target || this.idealTree + const actualTree = this.actualTree.target || this.actualTree + // we ONLY are allowed to make changes in the global top-level + // children where there's an explicit request. + for (const name of this[_explicitRequests]) { + const ideal = idealTree.children.get(name) + if (ideal) + filterNodes.push(ideal) + const actual = actualTree.children.get(name) + if (actual) + filterNodes.push(actual) + } + } else { + for (const ws of this[_workspaces]) { + const ideal = this.idealTree.children.get(ws) + if (ideal) + filterNodes.push(ideal) + const actual = this.actualTree.children.get(ws) + if (actual) + filterNodes.push(actual) + } + } + // find all the nodes that need to change between the actual // and ideal trees. this.diff = Diff.calculate({ + filterNodes, actual: this.actualTree, ideal: this.idealTree, }) @@ -970,20 +1001,85 @@ module.exports = cls => class Reifier extends cls { return meta.save(saveOpt) } - [_copyIdealToActual] () { + async [_copyIdealToActual] () { + // clean up any trash that is still in the tree + for (const path of this[_trashList]) { + const loc = relpath(this.idealTree.realpath, path) + const node = this.idealTree.inventory.get(loc) + if (node && node.root === this.idealTree) + node.parent = null + } + + // if we filtered to only certain nodes, then anything ELSE needs + // to be untouched in the resulting actual tree, even if it differs + // in the idealTree. Copy over anything that was in the actual and + // was not changed, delete anything in the ideal and not actual. + // Then we move the entire idealTree over to this.actualTree, and + // save the hidden lockfile. + if (this.diff && this.diff.filterSet.size) { + const { filterSet } = this.diff + const seen = new Set() + for (const [loc, ideal] of this.idealTree.inventory.entries()) { + if (seen.has(loc)) + continue + seen.add(loc) + + // if it's an ideal node from the filter set, then skip it + // because we already made whatever changes were necessary + if (filterSet.has(ideal)) + continue + + // otherwise, if it's not in the actualTree, then it's not a thing + // that we actually added. And if it IS in the actualTree, then + // it's something that we left untouched, so we need to record + // that. + const actual = this.actualTree.inventory.get(loc) + if (!actual) + ideal.root = null + else { + if ([...actual.linksIn].some(link => filterSet.has(link))) { + seen.add(actual.location) + continue + } + const { realpath, isLink } = actual + if (isLink && ideal.isLink && ideal.realpath === realpath) + continue + else + actual.root = this.idealTree + } + } + + // now find any actual nodes that may not be present in the ideal + // tree, but were left behind by virtue of not being in the filter + for (const [loc, actual] of this.actualTree.inventory.entries()) { + if (seen.has(loc)) + continue + seen.add(loc) + if (filterSet.has(actual)) + continue + actual.root = this.idealTree + } + + // prune out any tops that lack a linkIn + for (const top of this.idealTree.tops) { + if (top.linksIn.size === 0) + top.root = null + } + + // need to calculate dep flags, since nodes may have been marked + // as extraneous or otherwise incorrect during transit. + calcDepFlags(this.idealTree) + } + // save the ideal's meta as a hidden lockfile after we actualize it this.idealTree.meta.filename = - this.path + '/node_modules/.package-lock.json' + this.idealTree.realpath + '/node_modules/.package-lock.json' this.idealTree.meta.hiddenLockfile = true + this.actualTree = this.idealTree this.idealTree = null - for (const path of this[_trashList]) { - const loc = relpath(this.path, path) - const node = this.actualTree.inventory.get(loc) - if (node && node.root === this.actualTree) - node.parent = null - } - return !this[_global] && this.actualTree.meta.save() + if (!this[_global]) + await this.actualTree.meta.save() } } diff --git a/lib/diff.js b/lib/diff.js index ada67f816..84a8bae41 100644 --- a/lib/diff.js +++ b/lib/diff.js @@ -11,7 +11,8 @@ const {existsSync} = require('fs') const ssri = require('ssri') class Diff { - constructor ({actual, ideal}) { + constructor ({actual, ideal, filterSet}) { + this.filterSet = filterSet this.children = [] this.actual = actual this.ideal = ideal @@ -29,9 +30,54 @@ class Diff { this.removed = [] } - static calculate ({actual, ideal}) { + static calculate ({actual, ideal, filterNodes = []}) { + // if there's a filterNode, then: + // - get the path from the root to the filterNode. The root or + // root.target should have an edge either to the filterNode or + // a link to the filterNode. If not, abort. Add the path to the + // filterSet. + // - Add set of Nodes depended on by the filterNode to filterSet. + // - Anything outside of that set should be ignored by getChildren + const filterSet = new Set() + for (const filterNode of filterNodes) { + const { root } = filterNode + if (root !== ideal && root !== actual) + throw new Error('invalid filterNode: outside idealTree/actualTree') + const { target } = root + const rootTarget = target || root + const edge = [...rootTarget.edgesOut.values()].filter(e => { + return e.to && (e.to === filterNode || e.to.target === filterNode) + })[0] + filterSet.add(root) + filterSet.add(rootTarget) + filterSet.add(ideal) + filterSet.add(actual) + if (edge && edge.to) { + filterSet.add(edge.to) + if (edge.to.target) + filterSet.add(edge.to.target) + } + filterSet.add(filterNode) + + depth({ + tree: filterNode, + visit: node => filterSet.add(node), + getChildren: node => { + node = node.target || node + const loc = node.location + const idealNode = ideal.inventory.get(loc) + const ideals = !idealNode ? [] + : [...idealNode.edgesOut.values()].filter(e => e.to).map(e => e.to) + const actualNode = actual.inventory.get(loc) + const actuals = !actualNode ? [] + : [...actualNode.edgesOut.values()].filter(e => e.to).map(e => e.to) + return ideals.concat(actuals) + }, + }) + } + return depth({ - tree: new Diff({actual, ideal}), + tree: new Diff({actual, ideal, filterSet}), getChildren, leave, }) @@ -89,20 +135,20 @@ const allChildren = node => { // to create the diff tree const getChildren = diff => { const children = [] - const {unchanged, removed} = diff + const {actual, ideal, unchanged, removed, filterSet} = diff // Note: we DON'T diff fsChildren themselves, because they are either // included in the package contents, or part of some other project, and // will never appear in legacy shrinkwraps anyway. but we _do_ include the // child nodes of fsChildren, because those are nodes that we are typically // responsible for installing. - const actualKids = allChildren(diff.actual) - const idealKids = allChildren(diff.ideal) + const actualKids = allChildren(actual) + const idealKids = allChildren(ideal) const paths = new Set([...actualKids.keys(), ...idealKids.keys()]) for (const path of paths) { const actual = actualKids.get(path) const ideal = idealKids.get(path) - diffNode(actual, ideal, children, unchanged, removed) + diffNode(actual, ideal, children, unchanged, removed, filterSet) } if (diff.leaves && !children.length) @@ -111,7 +157,10 @@ const getChildren = diff => { return children } -const diffNode = (actual, ideal, children, unchanged, removed) => { +const diffNode = (actual, ideal, children, unchanged, removed, filterSet) => { + if (filterSet.size && !(filterSet.has(ideal) || filterSet.has(actual))) + return + const action = getAction({actual, ideal}) // if it's a match, then get its children @@ -119,7 +168,7 @@ const diffNode = (actual, ideal, children, unchanged, removed) => { if (action) { if (action === 'REMOVE') removed.push(actual) - children.push(new Diff({actual, ideal})) + children.push(new Diff({actual, ideal, filterSet})) } else { unchanged.push(ideal) // !*! Weird dirty hack warning !*! @@ -150,7 +199,7 @@ const diffNode = (actual, ideal, children, unchanged, removed) => { for (const node of bundledChildren) node.parent = ideal } - children.push(...getChildren({actual, ideal, unchanged, removed})) + children.push(...getChildren({actual, ideal, unchanged, removed, filterSet})) } } diff --git a/lib/node.js b/lib/node.js index fa39bed5e..197804e0c 100644 --- a/lib/node.js +++ b/lib/node.js @@ -685,6 +685,7 @@ class Node { ...this.children.values(), ...this.inventory.values(), ].filter(n => n !== this)) + for (const child of family) { if (child.root !== root) { child[_delistFromMeta]() @@ -704,12 +705,14 @@ class Node { } // if we had a target, and didn't find one in the new root, then bring - // it over as well. - if (this.isLink && target && !this.target) + // it over as well, but only if we're setting the link into a new root, + // as we don't want to lose the target any time we remove a link. + if (this.isLink && target && !this.target && root !== this) target.root = root // tree should always be valid upon root setter completion. treeCheck(this) + treeCheck(root) } get root () { diff --git a/lib/printable.js b/lib/printable.js index 588121dbc..b39a53cc5 100644 --- a/lib/printable.js +++ b/lib/printable.js @@ -126,6 +126,9 @@ class EdgeIn extends Edge { } const printableTree = (tree, path = []) => { + if (!tree) + return tree + const Cls = tree.isLink ? ArboristLink : tree.sourceReference ? ArboristVirtualNode : ArboristNode diff --git a/notes/filtered-reify.md b/notes/filtered-reify.md new file mode 100644 index 000000000..169f0a1f7 --- /dev/null +++ b/notes/filtered-reify.md @@ -0,0 +1,93 @@ +# filtered reify + +Use case: call `Arborist.reify()` but _only_ reify nodes that are the +dependencies of one specific node. + +Examples: + +- Global reification: we don't want to have to read the entire global space + to install a new package globally. Anything that is not in the + `explicitRequests` list should be ignored. Since `global` triggers + `globalStyle` bundling, this means that effectively any other top-level + node should be untouched. Then if it is present in the idealTree but not + in the actualTree, we don't end up deleting it, which has been the source + of some very unfortunate bugs. + +- Workspace reification: we want to be able to say `npm i -w foo -w bar` to + only install specific workspaces and their (possibly hoisted/shared) + dependencies. + +The goal here would be that if we accidentally have more nodes in the +actualTree that are outside of the filter, we don't accidentally remove +them, or if we have nodes in the idealTree that are outside the filter, we +don't accidentally add/change them. + +## approach 1 - limit actualTree and idealTree to filtered subtree + +This is closest to the current behavior with respect to global installs. +However, rather than relying on starting with a filtered actualTree, and +trusting that we only built the idealTree correctly, we'd add a step where +we explicitly filter/prune both trees to only include the dependencies of +the starting Node. + +Advantage is that reify and diff can remain unaware of dependency graphs. +Diff continues to just calculate the difference between trees, and reify +just makes the changes required to turn the existing actual nodes into the +ideal nodes. + +Would avoid previous global install problems by adding an extra step to +prevent any accidental inclusion of nodes that are outside of the expected +set. + +This would still require that the idealTree be built up in its entirety for +workspace installs, because some dependencies might be hoisted or shared, +and the idealTree that is _saved_ must still reflect these. + +So, how to save the idealTree in its entirety, but only Diff the filtered +set? Some kind of flag to tell Diff to ignore it? The value of this +approach is that Diff doesn't have to know about the dependency +relationships, so if we have to put duct tape on it to communicate those +relationships out of band, then that's not great. Maybe keep two +idealTree's around, one with the full set, and one that's filtered down to +just the bits we want to diff/install? + +## approach 2 - make Diff dependency-graph-aware + +This avoids having to maintain two idealTree instances, or do a subsequent +step to prune the trees before letting reify/diff operate on them, and +preserves the ability for reify() to remain dependency-agnostic. + +Instead of starting from the root and walking `children` and `fsChildren`, +the Diff algorithm would start from the root node, and walk only to the +target filter node, and then walk through the `edgesOut` to find nodes that +need to be diffed. + +### one pass + +Instead of walking the physical `children`/`fsChildren` tree, _only_ walk +the `edgesOut` and build up the diff object that way. + +The challenge here will be that we may end up several layers deep in the +physical tree, and then have to add a node that is shallower. So that +makes the diff walking algorithm a bit more complicated, because we're not +just slapping a new object into a child set, but may find ourselves +back-tracking up the tree as the graph wanders around. + +### two pass + +Make a first pass to create the set of nodes based on the edgesOut +traversal. Then do the current physical-tree based differencing walk, but +skipping any nodes that are not in the set. + +This is potentially expensive if the trees are very large (eg, a project +with thousands of workspaces), but is safe and very low complexity. + +Start with this. + +## approach 3 - make Reify dependency-graph-aware + +Given the existing complexity of reify(), making it have to look at the +dependency graph is a pretty big paradigm shift. Currently, the one thing +that makes it reasonable to reason about, is that while the algorithm is +somewhat involved, it really only has one job (turn one representation of a +tree into another). diff --git a/package-lock.json b/package-lock.json index 15b2a80f1..c5fe8cef8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "cacache": "^15.0.3", "common-ancestor-path": "^1.0.1", "json-parse-even-better-errors": "^2.3.1", - "json-stringify-nice": "^1.1.1", + "json-stringify-nice": "^1.1.2", "mkdirp-infer-owner": "^2.0.0", "npm-install-checks": "^4.0.0", "npm-package-arg": "^8.1.0", @@ -2919,9 +2919,9 @@ "dev": true }, "node_modules/json-stringify-nice": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/json-stringify-nice/-/json-stringify-nice-1.1.1.tgz", - "integrity": "sha512-aHOgcSoOLvmFZQMvZ27rFw68r4e9OlQtH7YEcF2u5amVYbF/D3cKBXKCvl5EGhQz2NwJZ6RPfgRX6yNQ+UBKJw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/json-stringify-nice/-/json-stringify-nice-1.1.2.tgz", + "integrity": "sha512-mc0EsmCq4Ru6jTdKtKvzKzGJPa7eUHXe5/WAprXwyYYR1iY2qTcvaw3QBkPKGfJYvRr5vXoaIvMtttM+/f1xOA==", "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -9893,9 +9893,9 @@ "dev": true }, "json-stringify-nice": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/json-stringify-nice/-/json-stringify-nice-1.1.1.tgz", - "integrity": "sha512-aHOgcSoOLvmFZQMvZ27rFw68r4e9OlQtH7YEcF2u5amVYbF/D3cKBXKCvl5EGhQz2NwJZ6RPfgRX6yNQ+UBKJw==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/json-stringify-nice/-/json-stringify-nice-1.1.2.tgz", + "integrity": "sha512-mc0EsmCq4Ru6jTdKtKvzKzGJPa7eUHXe5/WAprXwyYYR1iY2qTcvaw3QBkPKGfJYvRr5vXoaIvMtttM+/f1xOA==" }, "json-stringify-safe": { "version": "5.0.1", diff --git a/package.json b/package.json index e745be2c7..fb835d4a6 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "cacache": "^15.0.3", "common-ancestor-path": "^1.0.1", "json-parse-even-better-errors": "^2.3.1", - "json-stringify-nice": "^1.1.1", + "json-stringify-nice": "^1.1.2", "mkdirp-infer-owner": "^2.0.0", "npm-install-checks": "^4.0.0", "npm-package-arg": "^8.1.0", diff --git a/tap-snapshots/test-arborist-build-ideal-tree.js-TAP.test.js b/tap-snapshots/test-arborist-build-ideal-tree.js-TAP.test.js index 53c287896..c65c852ad 100644 --- a/tap-snapshots/test-arborist-build-ideal-tree.js-TAP.test.js +++ b/tap-snapshots/test-arborist-build-ideal-tree.js-TAP.test.js @@ -107277,6 +107277,87 @@ ArboristNode { } ` +exports[`test/arborist/build-ideal-tree.js TAP replace a link with a matching link when the current one is wrong > replace incorrect with correct 1`] = ` +ArboristNode { + "children": Map { + "x" => ArboristLink { + "edgesIn": Set { + EdgeIn { + "from": "", + "name": "x", + "spec": "file:correct/x", + "type": "prod", + }, + }, + "location": "node_modules/x", + "name": "x", + "path": "{CWD}/test/arborist/build-ideal-tree-replace-a-link-with-a-matching-link-when-the-current-one-is-wrong/node_modules/x", + "realpath": "{CWD}/test/arborist/build-ideal-tree-replace-a-link-with-a-matching-link-when-the-current-one-is-wrong/correct/x", + "resolved": "file:../correct/x", + "target": ArboristNode { + "location": "correct/x", + }, + "version": "1.2.3", + }, + "y" => ArboristLink { + "edgesIn": Set { + EdgeIn { + "from": "", + "name": "y", + "spec": "file:correct/x", + "type": "prod", + }, + }, + "location": "node_modules/y", + "name": "y", + "packageName": "x", + "path": "{CWD}/test/arborist/build-ideal-tree-replace-a-link-with-a-matching-link-when-the-current-one-is-wrong/node_modules/y", + "realpath": "{CWD}/test/arborist/build-ideal-tree-replace-a-link-with-a-matching-link-when-the-current-one-is-wrong/correct/x", + "resolved": "file:../correct/x", + "target": ArboristNode { + "location": "correct/x", + }, + "version": "1.2.3", + }, + }, + "edgesOut": Map { + "x" => EdgeOut { + "name": "x", + "spec": "file:correct/x", + "to": "node_modules/x", + "type": "prod", + }, + "y" => EdgeOut { + "name": "y", + "spec": "file:correct/x", + "to": "node_modules/y", + "type": "prod", + }, + }, + "fsChildren": Set { + ArboristNode { + "location": "correct/x", + "name": "x", + "path": "{CWD}/test/arborist/build-ideal-tree-replace-a-link-with-a-matching-link-when-the-current-one-is-wrong/correct/x", + "version": "1.2.3", + }, + ArboristNode { + "dev": true, + "extraneous": true, + "location": "incorrect/x", + "name": "x", + "optional": true, + "path": "{CWD}/test/arborist/build-ideal-tree-replace-a-link-with-a-matching-link-when-the-current-one-is-wrong/incorrect/x", + "peer": true, + "version": "1.2.3", + }, + }, + "location": "", + "name": "build-ideal-tree-replace-a-link-with-a-matching-link-when-the-current-one-is-wrong", + "path": "{CWD}/test/arborist/build-ideal-tree-replace-a-link-with-a-matching-link-when-the-current-one-is-wrong", +} +` + exports[`test/arborist/build-ideal-tree.js TAP respect the yarn.lock file > expect resolving Promise 1`] = ` ArboristNode { "children": Map { diff --git a/tap-snapshots/test-arborist-reify.js-TAP.test.js b/tap-snapshots/test-arborist-reify.js-TAP.test.js index ed3803fca..7ff27319f 100644 --- a/tap-snapshots/test-arborist-reify.js-TAP.test.js +++ b/tap-snapshots/test-arborist-reify.js-TAP.test.js @@ -1619,6 +1619,810 @@ ArboristNode { } ` +exports[`test/arborist/reify.js TAP filtered reification in workspaces > hidden lockfile - c 1`] = ` +{ + "name": "reify-filtered-reification-in-workspaces", + "lockfileVersion": 2, + "requires": true, + "packages": { + "node_modules/c": { + "resolved": "packages/c", + "link": true + }, + "packages/c": { + "version": "1.2.3", + "dependencies": { + "wrappy": "1.0.0" + } + }, + "packages/c/node_modules/wrappy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.0.tgz", + "integrity": "sha1-iq5PxrTNa+MqRVOYW88ys+4THk4=" + } + } +} + +` + +exports[`test/arborist/reify.js TAP filtered reification in workspaces > hidden lockfile - c, old x, removed a 1`] = ` +{ + "name": "reify-filtered-reification-in-workspaces", + "lockfileVersion": 2, + "requires": true, + "packages": { + "apps/x": { + "version": "1.2.3" + }, + "node_modules/c": { + "resolved": "packages/c", + "link": true + }, + "node_modules/x": { + "resolved": "apps/x", + "link": true + }, + "packages/c": { + "version": "1.2.3", + "dependencies": { + "wrappy": "1.0.0" + } + }, + "packages/c/node_modules/wrappy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.0.tgz", + "integrity": "sha1-iq5PxrTNa+MqRVOYW88ys+4THk4=" + } + } +} + +` + +exports[`test/arborist/reify.js TAP filtered reification in workspaces > hidden lockfile - c, x 1`] = ` +{ + "name": "reify-filtered-reification-in-workspaces", + "lockfileVersion": 2, + "requires": true, + "packages": { + "apps/x": { + "version": "1.2.3" + }, + "node_modules/c": { + "resolved": "packages/c", + "link": true + }, + "node_modules/x": { + "resolved": "apps/x", + "link": true + }, + "packages/c": { + "version": "1.2.3", + "dependencies": { + "wrappy": "1.0.0" + } + } + } +} + +` + +exports[`test/arborist/reify.js TAP filtered reification in workspaces > hidden lockfile - c, x, a 1`] = ` +{ + "name": "reify-filtered-reification-in-workspaces", + "lockfileVersion": 2, + "requires": true, + "packages": { + "apps/x": { + "version": "1.2.3" + }, + "node_modules/a": { + "resolved": "packages/a", + "link": true + }, + "node_modules/c": { + "resolved": "packages/c", + "link": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/x": { + "resolved": "apps/x", + "link": true + }, + "packages/a": { + "version": "1.2.3", + "dependencies": { + "once": "", + "wrappy": "1.0.2" + } + }, + "packages/c": { + "version": "1.2.3", + "dependencies": { + "wrappy": "1.0.0" + } + } + } +} + +` + +exports[`test/arborist/reify.js TAP filtered reification in workspaces > hidden lockfile - foo/x linked, c, old x, removed a 1`] = ` +{ + "name": "reify-filtered-reification-in-workspaces", + "lockfileVersion": 2, + "requires": true, + "packages": { + "apps/x": { + "version": "1.2.3" + }, + "foo/x": { + "version": "1.2.3", + "extraneous": true + }, + "node_modules/c": { + "resolved": "packages/c", + "link": true + }, + "node_modules/foox": { + "resolved": "foo/x", + "link": true + }, + "node_modules/x": { + "resolved": "apps/x", + "link": true + }, + "packages/c": { + "version": "1.2.3", + "dependencies": { + "wrappy": "1.0.0" + } + }, + "packages/c/node_modules/wrappy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.0.tgz", + "integrity": "sha1-iq5PxrTNa+MqRVOYW88ys+4THk4=" + } + } +} + +` + +exports[`test/arborist/reify.js TAP filtered reification in workspaces > reify the a workspace after reifying c 1`] = ` +ArboristNode { + "children": Map { + "a" => ArboristLink { + "edgesIn": Set { + EdgeIn { + "from": "", + "name": "a", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/a", + "type": "workspace", + }, + }, + "location": "node_modules/a", + "name": "a", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/node_modules/a", + "realpath": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/a", + "resolved": "file:../packages/a", + "target": ArboristNode { + "location": "packages/a", + }, + "version": "1.2.3", + }, + "c" => ArboristLink { + "edgesIn": Set { + EdgeIn { + "from": "", + "name": "c", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/c", + "type": "workspace", + }, + }, + "location": "node_modules/c", + "name": "c", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/node_modules/c", + "realpath": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/c", + "resolved": "file:../packages/c", + "target": ArboristNode { + "location": "packages/c", + }, + "version": "1.2.3", + }, + "once" => ArboristNode { + "edgesIn": Set { + EdgeIn { + "from": "packages/a", + "name": "once", + "spec": "*", + "type": "prod", + }, + }, + "edgesOut": Map { + "wrappy" => EdgeOut { + "name": "wrappy", + "spec": "1", + "to": "node_modules/wrappy", + "type": "prod", + }, + }, + "location": "node_modules/once", + "name": "once", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/node_modules/once", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "version": "1.4.0", + }, + "wrappy" => ArboristNode { + "edgesIn": Set { + EdgeIn { + "from": "node_modules/once", + "name": "wrappy", + "spec": "1", + "type": "prod", + }, + EdgeIn { + "from": "packages/a", + "name": "wrappy", + "spec": "1.0.2", + "type": "prod", + }, + EdgeIn { + "error": "INVALID", + "from": "packages/c", + "name": "wrappy", + "spec": "1.0.0", + "type": "prod", + }, + }, + "location": "node_modules/wrappy", + "name": "wrappy", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/node_modules/wrappy", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "version": "1.0.2", + }, + "x" => ArboristLink { + "edgesIn": Set { + EdgeIn { + "from": "", + "name": "x", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/apps/x", + "type": "workspace", + }, + }, + "location": "node_modules/x", + "name": "x", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/node_modules/x", + "realpath": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/apps/x", + "resolved": "file:../apps/x", + "target": ArboristNode { + "location": "apps/x", + }, + "version": "1.2.3", + }, + }, + "edgesOut": Map { + "a" => EdgeOut { + "name": "a", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/a", + "to": "node_modules/a", + "type": "workspace", + }, + "b" => EdgeOut { + "error": "MISSING", + "name": "b", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/b", + "to": null, + "type": "workspace", + }, + "c" => EdgeOut { + "name": "c", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/c", + "to": "node_modules/c", + "type": "workspace", + }, + "x" => EdgeOut { + "name": "x", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/apps/x", + "to": "node_modules/x", + "type": "workspace", + }, + }, + "fsChildren": Set { + ArboristNode { + "location": "apps/x", + "name": "x", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/apps/x", + "version": "1.2.3", + }, + ArboristNode { + "edgesOut": Map { + "once" => EdgeOut { + "name": "once", + "spec": "*", + "to": "node_modules/once", + "type": "prod", + }, + "wrappy" => EdgeOut { + "name": "wrappy", + "spec": "1.0.2", + "to": "node_modules/wrappy", + "type": "prod", + }, + }, + "location": "packages/a", + "name": "a", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/a", + "version": "1.2.3", + }, + ArboristNode { + "edgesOut": Map { + "wrappy" => EdgeOut { + "error": "INVALID", + "name": "wrappy", + "spec": "1.0.0", + "to": "node_modules/wrappy", + "type": "prod", + }, + }, + "location": "packages/c", + "name": "c", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/c", + "version": "1.2.3", + }, + }, + "location": "", + "name": "reify-filtered-reification-in-workspaces", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces", +} +` + +exports[`test/arborist/reify.js TAP filtered reification in workspaces > reify the c workspace only 1`] = ` +ArboristNode { + "children": Map { + "c" => ArboristLink { + "edgesIn": Set { + EdgeIn { + "from": "", + "name": "c", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/c", + "type": "workspace", + }, + }, + "location": "node_modules/c", + "name": "c", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/node_modules/c", + "realpath": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/c", + "resolved": "file:../packages/c", + "target": ArboristNode { + "location": "packages/c", + }, + "version": "1.2.3", + }, + }, + "edgesOut": Map { + "a" => EdgeOut { + "error": "MISSING", + "name": "a", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/a", + "to": null, + "type": "workspace", + }, + "b" => EdgeOut { + "error": "MISSING", + "name": "b", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/b", + "to": null, + "type": "workspace", + }, + "c" => EdgeOut { + "name": "c", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/c", + "to": "node_modules/c", + "type": "workspace", + }, + "x" => EdgeOut { + "error": "MISSING", + "name": "x", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/apps/x", + "to": null, + "type": "workspace", + }, + }, + "fsChildren": Set { + ArboristNode { + "children": Map { + "wrappy" => ArboristNode { + "edgesIn": Set { + EdgeIn { + "from": "packages/c", + "name": "wrappy", + "spec": "1.0.0", + "type": "prod", + }, + }, + "location": "packages/c/node_modules/wrappy", + "name": "wrappy", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/c/node_modules/wrappy", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.0.tgz", + "version": "1.0.0", + }, + }, + "edgesOut": Map { + "wrappy" => EdgeOut { + "name": "wrappy", + "spec": "1.0.0", + "to": "packages/c/node_modules/wrappy", + "type": "prod", + }, + }, + "location": "packages/c", + "name": "c", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/c", + "version": "1.2.3", + }, + }, + "location": "", + "name": "reify-filtered-reification-in-workspaces", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces", +} +` + +exports[`test/arborist/reify.js TAP filtered reification in workspaces > reify the workspaces, foo/x linked, c, old x, removed a 1`] = ` +ArboristNode { + "children": Map { + "c" => ArboristLink { + "edgesIn": Set { + EdgeIn { + "from": "", + "name": "c", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/c", + "type": "workspace", + }, + }, + "location": "node_modules/c", + "name": "c", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/node_modules/c", + "realpath": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/c", + "resolved": "file:../packages/c", + "target": ArboristNode { + "location": "packages/c", + }, + "version": "1.2.3", + }, + "foox" => ArboristLink { + "dev": true, + "extraneous": true, + "location": "node_modules/foox", + "name": "foox", + "optional": true, + "packageName": "x", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/node_modules/foox", + "peer": true, + "realpath": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/foo/x", + "resolved": "file:../foo/x", + "target": ArboristNode { + "location": "foo/x", + }, + "version": "1.2.3", + }, + "x" => ArboristLink { + "edgesIn": Set { + EdgeIn { + "error": "INVALID", + "from": "", + "name": "x", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/foo/x", + "type": "workspace", + }, + }, + "location": "node_modules/x", + "name": "x", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/node_modules/x", + "realpath": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/apps/x", + "resolved": "file:../apps/x", + "target": ArboristNode { + "location": "apps/x", + }, + "version": "1.2.3", + }, + }, + "edgesOut": Map { + "b" => EdgeOut { + "error": "MISSING", + "name": "b", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/b", + "to": null, + "type": "workspace", + }, + "c" => EdgeOut { + "name": "c", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/c", + "to": "node_modules/c", + "type": "workspace", + }, + "x" => EdgeOut { + "error": "INVALID", + "name": "x", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/foo/x", + "to": "node_modules/x", + "type": "workspace", + }, + }, + "fsChildren": Set { + ArboristNode { + "location": "apps/x", + "name": "x", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/apps/x", + "version": "1.2.3", + }, + ArboristNode { + "dev": true, + "extraneous": true, + "location": "foo/x", + "name": "x", + "optional": true, + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/foo/x", + "peer": true, + "version": "1.2.3", + }, + ArboristNode { + "children": Map { + "wrappy" => ArboristNode { + "edgesIn": Set { + EdgeIn { + "from": "packages/c", + "name": "wrappy", + "spec": "1.0.0", + "type": "prod", + }, + }, + "location": "packages/c/node_modules/wrappy", + "name": "wrappy", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/c/node_modules/wrappy", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.0.tgz", + "version": "1.0.0", + }, + }, + "edgesOut": Map { + "wrappy" => EdgeOut { + "name": "wrappy", + "spec": "1.0.0", + "to": "packages/c/node_modules/wrappy", + "type": "prod", + }, + }, + "location": "packages/c", + "name": "c", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/c", + "version": "1.2.3", + }, + }, + "location": "", + "name": "reify-filtered-reification-in-workspaces", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces", +} +` + +exports[`test/arborist/reify.js TAP filtered reification in workspaces > reify the workspaces, removing a and leaving c and old x in place 1`] = ` +ArboristNode { + "children": Map { + "c" => ArboristLink { + "edgesIn": Set { + EdgeIn { + "from": "", + "name": "c", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/c", + "type": "workspace", + }, + }, + "location": "node_modules/c", + "name": "c", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/node_modules/c", + "realpath": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/c", + "resolved": "file:../packages/c", + "target": ArboristNode { + "location": "packages/c", + }, + "version": "1.2.3", + }, + "x" => ArboristLink { + "edgesIn": Set { + EdgeIn { + "error": "INVALID", + "from": "", + "name": "x", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/foo/x", + "type": "workspace", + }, + }, + "location": "node_modules/x", + "name": "x", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/node_modules/x", + "realpath": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/apps/x", + "resolved": "file:../apps/x", + "target": ArboristNode { + "location": "apps/x", + }, + "version": "1.2.3", + }, + }, + "edgesOut": Map { + "b" => EdgeOut { + "error": "MISSING", + "name": "b", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/b", + "to": null, + "type": "workspace", + }, + "c" => EdgeOut { + "name": "c", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/c", + "to": "node_modules/c", + "type": "workspace", + }, + "x" => EdgeOut { + "error": "INVALID", + "name": "x", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/foo/x", + "to": "node_modules/x", + "type": "workspace", + }, + }, + "fsChildren": Set { + ArboristNode { + "location": "apps/x", + "name": "x", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/apps/x", + "version": "1.2.3", + }, + ArboristNode { + "children": Map { + "wrappy" => ArboristNode { + "edgesIn": Set { + EdgeIn { + "from": "packages/c", + "name": "wrappy", + "spec": "1.0.0", + "type": "prod", + }, + }, + "location": "packages/c/node_modules/wrappy", + "name": "wrappy", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/c/node_modules/wrappy", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.0.tgz", + "version": "1.0.0", + }, + }, + "edgesOut": Map { + "wrappy" => EdgeOut { + "name": "wrappy", + "spec": "1.0.0", + "to": "packages/c/node_modules/wrappy", + "type": "prod", + }, + }, + "location": "packages/c", + "name": "c", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/c", + "version": "1.2.3", + }, + }, + "location": "", + "name": "reify-filtered-reification-in-workspaces", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces", +} +` + +exports[`test/arborist/reify.js TAP filtered reification in workspaces > reify the x workspace after reifying c 1`] = ` +ArboristNode { + "children": Map { + "c" => ArboristLink { + "edgesIn": Set { + EdgeIn { + "from": "", + "name": "c", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/c", + "type": "workspace", + }, + }, + "location": "node_modules/c", + "name": "c", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/node_modules/c", + "realpath": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/c", + "resolved": "file:../packages/c", + "target": ArboristNode { + "location": "packages/c", + }, + "version": "1.2.3", + }, + "x" => ArboristLink { + "edgesIn": Set { + EdgeIn { + "from": "", + "name": "x", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/apps/x", + "type": "workspace", + }, + }, + "location": "node_modules/x", + "name": "x", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/node_modules/x", + "realpath": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/apps/x", + "resolved": "file:../apps/x", + "target": ArboristNode { + "location": "apps/x", + }, + "version": "1.2.3", + }, + }, + "edgesOut": Map { + "a" => EdgeOut { + "error": "MISSING", + "name": "a", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/a", + "to": null, + "type": "workspace", + }, + "b" => EdgeOut { + "error": "MISSING", + "name": "b", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/b", + "to": null, + "type": "workspace", + }, + "c" => EdgeOut { + "name": "c", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/c", + "to": "node_modules/c", + "type": "workspace", + }, + "x" => EdgeOut { + "name": "x", + "spec": "file:{CWD}/test/arborist/reify-filtered-reification-in-workspaces/apps/x", + "to": "node_modules/x", + "type": "workspace", + }, + }, + "fsChildren": Set { + ArboristNode { + "location": "apps/x", + "name": "x", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/apps/x", + "version": "1.2.3", + }, + ArboristNode { + "edgesOut": Map { + "wrappy" => EdgeOut { + "error": "MISSING", + "name": "wrappy", + "spec": "1.0.0", + "to": null, + "type": "prod", + }, + }, + "location": "packages/c", + "name": "c", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces/packages/c", + "version": "1.2.3", + }, + }, + "location": "", + "name": "reify-filtered-reification-in-workspaces", + "path": "{CWD}/test/arborist/reify-filtered-reification-in-workspaces", +} +` + exports[`test/arborist/reify.js TAP just the shrinkwrap cli-750-fresh > must match snapshot 1`] = ` { "name": "monorepo", diff --git a/tap-snapshots/test-diff.js-TAP.test.js b/tap-snapshots/test-diff.js-TAP.test.js index 577c7e595..59f78d68d 100644 --- a/tap-snapshots/test-diff.js-TAP.test.js +++ b/tap-snapshots/test-diff.js-TAP.test.js @@ -267,6 +267,183 @@ Diff { } ` +exports[`test/diff.js TAP filtered diff > c excluded, a and b present 1`] = ` +Diff { + "action": null, + "actual": Node { + "name": "path", + "path": "/project/path", + "integrity": null, + }, + "ideal": Node { + "name": "path", + "path": "/project/path", + "integrity": null, + }, + "leaves": Array [ + "/project/path/node_modules/a", + "/project/path/node_modules/b", + ], + "unchanged": Array [], + "removed": Array [], + "children": Array [ + Diff { + "action": "ADD", + "actual": undefined, + "ideal": Link { + "name": "a", + "path": "/project/path/node_modules/a", + "integrity": null, + }, + "leaves": Array [ + "/project/path/node_modules/a", + ], + "unchanged": Array [], + "removed": Array [], + "children": Array [], + }, + Diff { + "action": "ADD", + "actual": undefined, + "ideal": Node { + "name": "b", + "path": "/project/path/node_modules/b", + "integrity": null, + }, + "leaves": Array [ + "/project/path/node_modules/b", + ], + "unchanged": Array [], + "removed": Array [], + "children": Array [], + }, + ], +} +` + +exports[`test/diff.js TAP filtered diff > d is removed 1`] = ` +Diff { + "action": null, + "actual": Node { + "name": "path", + "path": "/project/path", + "integrity": null, + }, + "ideal": Node { + "name": "path", + "path": "/project/path", + "integrity": null, + }, + "leaves": Array [ + "/project/path/node_modules/d", + ], + "unchanged": Array [ + "/project/path/node_modules/a", + "/project/path/node_modules/b", + ], + "removed": Array [ + "/project/path/node_modules/d", + ], + "children": Array [ + Diff { + "action": "REMOVE", + "actual": Node { + "name": "d", + "path": "/project/path/node_modules/d", + "integrity": null, + }, + "ideal": undefined, + "leaves": Array [ + "/project/path/node_modules/d", + ], + "unchanged": Array [], + "removed": Array [], + "children": Array [], + }, + ], +} +` + +exports[`test/diff.js TAP filtered diff > e is removed (extraneous) 1`] = ` +Diff { + "action": null, + "actual": Node { + "name": "path", + "path": "/project/path", + "integrity": null, + }, + "ideal": Node { + "name": "path", + "path": "/project/path", + "integrity": null, + }, + "leaves": Array [ + "/project/path/node_modules/e", + ], + "unchanged": Array [], + "removed": Array [ + "/project/path/node_modules/e", + ], + "children": Array [ + Diff { + "action": "REMOVE", + "actual": Node { + "name": "e", + "path": "/project/path/node_modules/e", + "integrity": null, + }, + "ideal": undefined, + "leaves": Array [ + "/project/path/node_modules/e", + ], + "unchanged": Array [], + "removed": Array [], + "children": Array [], + }, + ], +} +` + +exports[`test/diff.js TAP filtered diff > e is removed 1`] = ` +Diff { + "action": null, + "actual": Node { + "name": "path", + "path": "/project/path", + "integrity": null, + }, + "ideal": Node { + "name": "path", + "path": "/project/path", + "integrity": null, + }, + "leaves": Array [ + "/project/path/node_modules/e", + ], + "unchanged": Array [], + "removed": Array [ + "/project/path/node_modules/e", + ], + "children": Array [ + Diff { + "action": "REMOVE", + "actual": Node { + "name": "e", + "path": "/project/path/node_modules/e", + "integrity": null, + }, + "ideal": undefined, + "leaves": Array [ + "/project/path/node_modules/e", + ], + "unchanged": Array [], + "removed": Array [], + "children": Array [], + }, + ], +} +` + exports[`test/diff.js TAP when a global root is a link, traverse the target children > correctly removes the child node 1`] = ` Diff { "action": null, diff --git a/tap-snapshots/test-printable.js-TAP.test.js b/tap-snapshots/test-printable.js-TAP.test.js index 54df0f12d..b797d46eb 100644 --- a/tap-snapshots/test-printable.js-TAP.test.js +++ b/tap-snapshots/test-printable.js-TAP.test.js @@ -5,6 +5,29 @@ * Make sure to inspect the output below. Do not ignore changes! */ 'use strict' +exports[`test/printable.js TAP broken links dont break the printing > must match snapshot 1`] = ` +{ +"children":Map{ +"devnull" => ArboristLink{ +"dev":true, +"extraneous":true, +"location":"node_modules/devnull", +"name":"devnull", +"optional":true, +"path":"/home/user/projects/root/node_modules/devnull", +"peer":true, +"realpath":"/home/user/projects/root/no/thing/here", +"resolved":"file:../no/thing/here", +"target":null,},}, +"dev":true, +"extraneous":true, +"location":"", +"name":"root", +"optional":true, +"path":"/home/user/projects/root", +"peer":true,} +` + exports[`test/printable.js TAP printable Node do not recurse forever > must match snapshot 1`] = ` { name:'recursive', diff --git a/test/arborist/build-ideal-tree.js b/test/arborist/build-ideal-tree.js index 208fae135..4ac0eef5c 100644 --- a/test/arborist/build-ideal-tree.js +++ b/test/arborist/build-ideal-tree.js @@ -2642,3 +2642,74 @@ t.test('allow a link dep to satisfy a peer dep', async t => { // avoids if the link dep is unmet t.matchSnapshot(await printIdeal(path + '/main', { add }), 'reified link avoids conflict') }) + +t.test('replace a link with a matching link when the current one is wrong', async t => { + const path = t.testdir({ + 'package.json': JSON.stringify({ + dependencies: { + // testing what happens when a user hand-edits the + // package.json to point to a different target with + // a matching package. + x: 'file:correct/x', + y: 'file:correct/x', + }, + }), + correct: { + x: { + 'package.json': JSON.stringify({ + name: 'x', + version: '1.2.3', + }), + }, + }, + incorrect: { + x: { + 'package.json': JSON.stringify({ + name: 'x', + version: '1.2.3', + }), + }, + }, + node_modules: { + x: t.fixture('symlink', '../incorrect/x'), + y: t.fixture('symlink', '../correct/x'), + }, + 'package-lock.json': JSON.stringify({ + lockfileVersion: 2, + requires: true, + packages: { + '': { + dependencies: { + x: 'file:incorrect/x', + y: 'file:correct/x', + }, + }, + 'incorrect/x': { + version: '1.2.3', + name: 'x', + }, + 'node_modules/y': { + resolved: 'correct/x', + link: true, + }, + 'correct/x': { + version: '1.2.3', + name: 'x', + }, + 'node_modules/x': { + resolved: 'incorrect/x', + link: true, + }, + }, + dependencies: { + y: { + version: 'file:correct/x', + }, + x: { + version: 'file:incorrect/x', + }, + }, + }), + }) + t.matchSnapshot(await printIdeal(path), 'replace incorrect with correct') +}) diff --git a/test/arborist/reify.js b/test/arborist/reify.js index 29be07c6d..1a8fa52b9 100644 --- a/test/arborist/reify.js +++ b/test/arborist/reify.js @@ -1503,3 +1503,132 @@ t.test('saving should not replace file: dep with version', async t => { t.equal(JSON.parse(pj2).dependencies.abbrev, 'file:abbrev', 'still a file: spec after a bare name install') }) + +t.test('filtered reification in workspaces', async t => { + const path = t.testdir({ + 'package.json': JSON.stringify({ + workspaces: [ + 'apps/*', + 'packages/*', + ], + }), + // 'apps' comes ahead of 'node_modules' alphabetically, + // included in the test so that we ensure that the copy + // over works properly in both directions. + apps: { + x: { + 'package.json': JSON.stringify({ + name: 'x', + version: '1.2.3', + }), + }, + }, + // this is going to be a workspace that we switch to in the ideal + // tree, but the actual tree will still be lagging behind. + foo: { + x: { + 'package.json': JSON.stringify({ + name: 'x', + version: '1.2.3', + }), + }, + }, + packages: { + a: { + 'package.json': JSON.stringify({ + name: 'a', + version: '1.2.3', + dependencies: { + once: '', + wrappy: '1.0.2', + }, + }), + }, + b: { + 'package.json': JSON.stringify({ + name: 'b', + version: '1.2.3', + dependencies: { + abbrev: '', + }, + }), + }, + c: { + 'package.json': JSON.stringify({ + name: 'c', + version: '1.2.3', + dependencies: { + wrappy: '1.0.0', + }, + }), + }, + }, + }) + + const hiddenLock = resolve(path, 'node_modules/.package-lock.json') + + t.matchSnapshot(await printReified(path, { workspaces: ['c'] }), + 'reify the c workspace only') + + t.matchSnapshot(fs.readFileSync(hiddenLock, 'utf8'), + 'hidden lockfile - c') + + t.matchSnapshot(await printReified(path, { workspaces: ['x'] }), + 'reify the x workspace after reifying c') + + t.matchSnapshot(fs.readFileSync(hiddenLock, 'utf8'), + 'hidden lockfile - c, x') + + t.matchSnapshot(await printReified(path, { workspaces: ['a'] }), + 'reify the a workspace after reifying c') + + t.matchSnapshot(fs.readFileSync(hiddenLock, 'utf8'), + 'hidden lockfile - c, x, a') + + // now remove the a workspace, and move x to a new target location, + // but we will not reify the apps->foo change. + fs.writeFileSync(`${path}/package.json`, JSON.stringify({ + workspaces: [ + 'foo/*', + 'packages/b', + 'packages/c', + ], + })) + + t.matchSnapshot(await printReified(path, { workspaces: ['a', 'c'] }), + 'reify the workspaces, removing a and leaving c and old x in place') + + t.matchSnapshot(fs.readFileSync(hiddenLock, 'utf8'), + 'hidden lockfile - c, old x, removed a') + + // Same thing, BUT, we now have a reason to already have the foo/x + // in fsChildren. fully reify with this package.json, then change + // the root package.json back to the test above. This exercises an + // edge case where the actual and ideal trees are somewhat out of sync, + // by virtue of the actualTree being generated with a package.json + // which has changed, but where only PART of the idealTree is reified + // over it. + fs.writeFileSync(`${path}/package.json`, JSON.stringify({ + dependencies: { + foox: 'file:foo/x', + }, + workspaces: [ + 'apps/x', + 'packages/a', + 'packages/c', + ], + })) + await reify(path) + fs.writeFileSync(`${path}/package.json`, JSON.stringify({ + workspaces: [ + 'foo/*', + 'packages/b', + 'packages/c', + ], + })) + t.matchSnapshot(await printReified(path, { workspaces: ['a', 'c'] }), + 'reify the workspaces, foo/x linked, c, old x, removed a') + + t.matchSnapshot(fs.readFileSync(hiddenLock, 'utf8'), + 'hidden lockfile - foo/x linked, c, old x, removed a') +}) diff --git a/test/diff.js b/test/diff.js index 90414d124..f497c038d 100644 --- a/test/diff.js +++ b/test/diff.js @@ -221,3 +221,128 @@ t.test('when a global root is a link, traverse the target children', async (t) = t.matchSnapshot(diff, 'correctly removes the child node') t.equal(diff.removed.length, 1, 'identifies the need to remove the child') }) + +t.test('filtered diff', async t => { + const ideal = new Node({ + path: '/project/path', + pkg: { + name: 'root', + dependencies: { a: 'file:a', c: '' }, + }, + }) + + new Link({ + parent: ideal, + pkg: {}, + realpath: '/project/path/a', + }) + const a = new Node({ + fsParent: ideal, + path: '/project/path/a', + pkg: { + name: 'a', + dependencies: { + b: '', + }, + }, + }) + new Node({ + parent: ideal, + pkg: { + name: 'b', + version: '1.2.3', + }, + }) + new Node({ + parent: ideal, + pkg: { + name: 'c', + version: '1.2.3', + }, + }) + + const actual = new Node({ + path: '/project/path', + pkg: { + name: 'root', + dependencies: { a: 'file:a', c: '' }, + }, + }) + + const cExcludedABPresent = Diff.calculate({actual, ideal, filterNodes: [a]}) + t.matchSnapshot(cExcludedABPresent, 'c excluded, a and b present') + + // make sure that *removing* something that *would* be depended on + // by the actual node in the filter set is also removed properly. + new Link({ + parent: actual, + pkg: {}, + realpath: '/project/path/a', + }) + new Node({ + fsParent: actual, + path: '/project/path/a', + pkg: { + name: 'a', + dependencies: { + b: '', + d: '', + }, + }, + }) + new Node({ + parent: actual, + pkg: { + name: 'b', + version: '1.2.3', + }, + }) + new Node({ + parent: actual, + pkg: { + name: 'd', + version: '1.2.3', + }, + }) + + const removeD = Diff.calculate({actual, ideal, filterNodes: [a]}) + t.matchSnapshot(removeD, 'd is removed') + + // removing a dependency, like we would with `npm rm -g foo` + actual.package = { + ...actual.package, + dependencies: { + ...actual.package.dependencies, + e: '*', + }, + } + const e = new Node({ + parent: actual, + pkg: { + name: 'e', + version: '1.2.3', + }, + }) + const eRemoved = Diff.calculate({actual, ideal, filterNodes: [e]}) + t.matchSnapshot(eRemoved, 'e is removed') + + // can't filter based on something that isn't there + t.throws(() => Diff.calculate({ + actual, + ideal, + filterNodes: [ + new Node({ + path: '/project/path/node_modules/x', + pkg: {name: 'x'}, + }), + ], + }), { + message: 'invalid filterNode: outside idealTree/actualTree', + }) + + // filtering an extraneous node is ok though + delete actual.package.dependencies.e + actual.package = { ...actual.package } + const eRemovedExtraneous = Diff.calculate({actual, ideal, filterNodes: [e]}) + t.matchSnapshot(eRemovedExtraneous, 'e is removed (extraneous)') +}) diff --git a/test/fixtures/registry-mocks/content/wrappy/-/wrappy-1.0.0.tgz b/test/fixtures/registry-mocks/content/wrappy/-/wrappy-1.0.0.tgz new file mode 100644 index 000000000..7855ddc30 Binary files /dev/null and b/test/fixtures/registry-mocks/content/wrappy/-/wrappy-1.0.0.tgz differ diff --git a/test/node.js b/test/node.js index fc19ac74c..588060413 100644 --- a/test/node.js +++ b/test/node.js @@ -591,8 +591,9 @@ t.test('load with a virtual filesystem parent', t => { t.equal(link.root, link, 'removed from parent, removed from root') t.equal(root.inventory.get(linkLoc), undefined, 'removed from root inventory') t.equal(link.inventory.has(link), true, 'link added to own inventory') - t.equal(link.target, linkTarget, 'target taken along for the ride') - t.equal(linkTarget.root, link, 'target rooted on link') + t.equal(link.target, null, 'target left behind when setting root to null') + linkTarget.root = link + t.equal(link.target, linkTarget, 'target set once roots match') t.equal(link.inventory.get(''), linkTarget) t.equal(root.edgesOut.get('link').error, 'MISSING') diff --git a/test/printable.js b/test/printable.js index de59b1ab7..016a93e59 100644 --- a/test/printable.js +++ b/test/printable.js @@ -226,3 +226,20 @@ t.test('virtual roots are shown with their sourceReference', t => { t.matchSnapshot(printable(virtual)) t.end() }) + +t.test('broken links dont break the printing', t => { + const tree = new Node({ + path: '/home/user/projects/root', + }) + + // a link with no target + const brokenLink = new Link({ + name: 'devnull', + realpath: '/home/user/projects/root/no/thing/here', + parent: tree, + }) + brokenLink.target.root = null + + t.matchSnapshot(printable(tree)) + t.end() +})