diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 69b72b640df87..4ea6d6f5e6239 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -618,6 +618,7 @@ graph LR; npmcli-arborist-->semver; npmcli-arborist-->ssri; npmcli-arborist-->tap; + npmcli-arborist-->tar-stream; npmcli-arborist-->tcompare; npmcli-arborist-->treeverse; npmcli-arborist-->walk-up-path; diff --git a/lib/utils/config/definitions.js b/lib/utils/config/definitions.js index 9ddbafd46f7bc..578e0daa67737 100644 --- a/lib/utils/config/definitions.js +++ b/lib/utils/config/definitions.js @@ -1090,14 +1090,14 @@ define('install-links', { define('install-strategy', { default: 'hoisted', - type: ['hoisted', 'nested', 'shallow'], + type: ['hoisted', 'nested', 'shallow', 'linked'], description: ` Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct deps at top-level. - linked: (coming soon) install in node_modules/.store, link in place, + linked: (experimental) install in node_modules/.store, link in place, unhoisted. `, flatten, diff --git a/node_modules/.gitignore b/node_modules/.gitignore index 9861a76e6a417..1a8e77b91f589 100644 --- a/node_modules/.gitignore +++ b/node_modules/.gitignore @@ -43,13 +43,13 @@ !/are-we-there-yet !/are-we-there-yet/node_modules/ /are-we-there-yet/node_modules/* -!/are-we-there-yet/node_modules/buffer !/are-we-there-yet/node_modules/readable-stream !/balanced-match !/base64-js !/bin-links !/binary-extensions !/brace-expansion +!/buffer !/builtins !/cacache !/chalk diff --git a/node_modules/are-we-there-yet/node_modules/buffer/AUTHORS.md b/node_modules/buffer/AUTHORS.md similarity index 100% rename from node_modules/are-we-there-yet/node_modules/buffer/AUTHORS.md rename to node_modules/buffer/AUTHORS.md diff --git a/node_modules/are-we-there-yet/node_modules/buffer/LICENSE b/node_modules/buffer/LICENSE similarity index 100% rename from node_modules/are-we-there-yet/node_modules/buffer/LICENSE rename to node_modules/buffer/LICENSE diff --git a/node_modules/are-we-there-yet/node_modules/buffer/index.d.ts b/node_modules/buffer/index.d.ts similarity index 100% rename from node_modules/are-we-there-yet/node_modules/buffer/index.d.ts rename to node_modules/buffer/index.d.ts diff --git a/node_modules/are-we-there-yet/node_modules/buffer/index.js b/node_modules/buffer/index.js similarity index 100% rename from node_modules/are-we-there-yet/node_modules/buffer/index.js rename to node_modules/buffer/index.js diff --git a/node_modules/are-we-there-yet/node_modules/buffer/package.json b/node_modules/buffer/package.json similarity index 100% rename from node_modules/are-we-there-yet/node_modules/buffer/package.json rename to node_modules/buffer/package.json diff --git a/package-lock.json b/package-lock.json index 84de274b55b45..2b8ab60f0139e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2776,29 +2776,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/are-we-there-yet/node_modules/buffer": { - "version": "6.0.3", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "inBundle": true, - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/are-we-there-yet/node_modules/readable-stream": { "version": "4.2.0", "inBundle": true, @@ -2921,6 +2898,12 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, + "node_modules/b4a": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.1.tgz", + "integrity": "sha512-AsKjNhz72yxteo/0EtQEiwkMUgk/tGmycXlbG4g3Ard2/ULtNLUykGOkeK0egmN27h0xMAhb76jYccW+XTBExA==", + "dev": true + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -3000,6 +2983,32 @@ "node": ">=10" } }, + "node_modules/bl": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.0.0.tgz", + "integrity": "sha512-Ik9BVIMdcWzSOCpzDv2XpQ4rJ4oZBuk3ck6MgiOv0EopdgtohN2uSCrrLlkH1Jf0KnpZZMBA3D0bUMbCdj/jgA==", + "dev": true, + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.3.0.tgz", + "integrity": "sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==", + "dev": true, + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -3054,6 +3063,30 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "inBundle": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -4964,6 +4997,12 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-fifo": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.1.0.tgz", + "integrity": "sha512-Kl29QoNbNvn4nhDsLYjyIAaIqaJB6rBx5p3sL9VjaefJ+eMFBWVZiaoguaoZfzEKr5RhAti0UgM8703akGPJ6g==", + "dev": true + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -9845,6 +9884,12 @@ ], "peer": true }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, "node_modules/quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", @@ -10930,6 +10975,16 @@ "escodegen": "^1.8.1" } }, + "node_modules/streamx": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.13.2.tgz", + "integrity": "sha512-+TWqixPhGDXEG9L/XczSbhfkmwAtGs3BJX5QNU6cvno+pOLKeszByWcnaTu6dg8efsTYqR8ZZuXWHhZfgrxMvA==", + "dev": true, + "dependencies": { + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "inBundle": true, @@ -13193,6 +13248,17 @@ "node": ">=10" } }, + "node_modules/tar-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.0.0.tgz", + "integrity": "sha512-O6OfUKBbQOqAhh6owTWmA730J/yZCYcpmZ1DBj2YX51ZQrt7d7NgzrR+CnO9wP6nt/viWZW2XeXLavX3/ZEbEg==", + "dev": true, + "dependencies": { + "b4a": "^1.6.1", + "bl": "^6.0.0", + "streamx": "^2.12.5" + } + }, "node_modules/tcompare": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-5.0.7.tgz", @@ -14351,6 +14417,7 @@ "minify-registry-metadata": "^3.0.0", "nock": "^13.2.0", "tap": "^16.3.2", + "tar-stream": "^3.0.0", "tcompare": "^5.0.6" }, "engines": { diff --git a/smoke-tests/tap-snapshots/test/index.js.test.cjs b/smoke-tests/tap-snapshots/test/index.js.test.cjs index de87748529e5f..662c831160eb2 100644 --- a/smoke-tests/tap-snapshots/test/index.js.test.cjs +++ b/smoke-tests/tap-snapshots/test/index.js.test.cjs @@ -55,9 +55,9 @@ npm ERR! npm ci npm ERR! npm ERR! Options: npm ERR! [-S|--save|--no-save|--save-prod|--save-dev|--save-optional|--save-peer|--save-bundle] -npm ERR! [-E|--save-exact] [-g|--global] [--install-strategy ] -npm ERR! [--legacy-bundling] [--global-style] -npm ERR! [--omit [--omit ...]] +npm ERR! [-E|--save-exact] [-g|--global] +npm ERR! [--install-strategy ] [--legacy-bundling] +npm ERR! [--global-style] [--omit [--omit ...]] npm ERR! [--strict-peer-deps] [--no-package-lock] [--foreground-scripts] npm ERR! [--ignore-scripts] [--no-audit] [--no-bin-links] [--no-fund] [--dry-run] npm ERR! [-w|--workspace [-w|--workspace ...]] diff --git a/tap-snapshots/test/lib/docs.js.test.cjs b/tap-snapshots/test/lib/docs.js.test.cjs index ad37be5218e1a..c882b320524c3 100644 --- a/tap-snapshots/test/lib/docs.js.test.cjs +++ b/tap-snapshots/test/lib/docs.js.test.cjs @@ -1163,13 +1163,13 @@ workspaces. #### \`install-strategy\` * Default: "hoisted" -* Type: "hoisted", "nested", or "shallow" +* Type: "hoisted", "nested", "shallow", or "linked" Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct -deps at top-level. linked: (coming soon) install in node_modules/.store, +deps at top-level. linked: (experimental) install in node_modules/.store, link in place, unhoisted. #### \`json\` @@ -2621,9 +2621,9 @@ npm ci Options: [-S|--save|--no-save|--save-prod|--save-dev|--save-optional|--save-peer|--save-bundle] -[-E|--save-exact] [-g|--global] [--install-strategy ] -[--legacy-bundling] [--global-style] -[--omit [--omit ...]] +[-E|--save-exact] [-g|--global] +[--install-strategy ] [--legacy-bundling] +[--global-style] [--omit [--omit ...]] [--strict-peer-deps] [--no-package-lock] [--foreground-scripts] [--ignore-scripts] [--no-audit] [--no-bin-links] [--no-fund] [--dry-run] [-w|--workspace [-w|--workspace ...]] @@ -2723,7 +2723,7 @@ Usage: npm dedupe Options: -[--install-strategy ] [--legacy-bundling] +[--install-strategy ] [--legacy-bundling] [--global-style] [--strict-peer-deps] [--no-package-lock] [--omit [--omit ...]] [--ignore-scripts] [--no-audit] [--no-bin-links] [--no-fund] [--dry-run] @@ -2994,7 +2994,7 @@ Usage: npm find-dupes Options: -[--install-strategy ] [--legacy-bundling] +[--install-strategy ] [--legacy-bundling] [--global-style] [--strict-peer-deps] [--no-package-lock] [--omit [--omit ...]] [--ignore-scripts] [--no-audit] [--no-bin-links] [--no-fund] @@ -3175,9 +3175,9 @@ npm install [ ...] Options: [-S|--save|--no-save|--save-prod|--save-dev|--save-optional|--save-peer|--save-bundle] -[-E|--save-exact] [-g|--global] [--install-strategy ] -[--legacy-bundling] [--global-style] -[--omit [--omit ...]] +[-E|--save-exact] [-g|--global] +[--install-strategy ] [--legacy-bundling] +[--global-style] [--omit [--omit ...]] [--strict-peer-deps] [--no-package-lock] [--foreground-scripts] [--ignore-scripts] [--no-audit] [--no-bin-links] [--no-fund] [--dry-run] [-w|--workspace [-w|--workspace ...]] @@ -3222,9 +3222,9 @@ npm install-ci-test Options: [-S|--save|--no-save|--save-prod|--save-dev|--save-optional|--save-peer|--save-bundle] -[-E|--save-exact] [-g|--global] [--install-strategy ] -[--legacy-bundling] [--global-style] -[--omit [--omit ...]] +[-E|--save-exact] [-g|--global] +[--install-strategy ] [--legacy-bundling] +[--global-style] [--omit [--omit ...]] [--strict-peer-deps] [--no-package-lock] [--foreground-scripts] [--ignore-scripts] [--no-audit] [--no-bin-links] [--no-fund] [--dry-run] [-w|--workspace [-w|--workspace ...]] @@ -3269,9 +3269,9 @@ npm install-test [ ...] Options: [-S|--save|--no-save|--save-prod|--save-dev|--save-optional|--save-peer|--save-bundle] -[-E|--save-exact] [-g|--global] [--install-strategy ] -[--legacy-bundling] [--global-style] -[--omit [--omit ...]] +[-E|--save-exact] [-g|--global] +[--install-strategy ] [--legacy-bundling] +[--global-style] [--omit [--omit ...]] [--strict-peer-deps] [--no-package-lock] [--foreground-scripts] [--ignore-scripts] [--no-audit] [--no-bin-links] [--no-fund] [--dry-run] [-w|--workspace [-w|--workspace ...]] @@ -3316,8 +3316,9 @@ npm link [] Options: [-S|--save|--no-save|--save-prod|--save-dev|--save-optional|--save-peer|--save-bundle] -[-E|--save-exact] [-g|--global] [--install-strategy ] -[--legacy-bundling] [--global-style] [--strict-peer-deps] [--no-package-lock] +[-E|--save-exact] [-g|--global] +[--install-strategy ] [--legacy-bundling] +[--global-style] [--strict-peer-deps] [--no-package-lock] [--omit [--omit ...]] [--ignore-scripts] [--no-audit] [--no-bin-links] [--no-fund] [--dry-run] [-w|--workspace [-w|--workspace ...]] @@ -4221,8 +4222,9 @@ npm update [...] Options: [-S|--save|--no-save|--save-prod|--save-dev|--save-optional|--save-peer|--save-bundle] -[-g|--global] [--install-strategy ] [--legacy-bundling] -[--global-style] [--omit [--omit ...]] +[-g|--global] [--install-strategy ] +[--legacy-bundling] [--global-style] +[--omit [--omit ...]] [--strict-peer-deps] [--no-package-lock] [--foreground-scripts] [--ignore-scripts] [--no-audit] [--no-bin-links] [--no-fund] [--dry-run] [-w|--workspace [-w|--workspace ...]] diff --git a/workspaces/arborist/lib/arborist/build-ideal-tree.js b/workspaces/arborist/lib/arborist/build-ideal-tree.js index a9c4b4bc0bb6d..2ea66ac336414 100644 --- a/workspaces/arborist/lib/arborist/build-ideal-tree.js +++ b/workspaces/arborist/lib/arborist/build-ideal-tree.js @@ -1232,6 +1232,7 @@ This is a one-time fix-up, please be patient... const isWorkspace = this.idealTree.workspaces && this.idealTree.workspaces.has(spec.name) // spec is a directory, link it unless installLinks is set or it's a workspace + // TODO post arborist refactor, will need to check for installStrategy=linked if (spec.type === 'directory' && (isWorkspace || !installLinks)) { return this[_linkFromSpec](name, spec, parent, edge) } diff --git a/workspaces/arborist/lib/arborist/index.js b/workspaces/arborist/lib/arborist/index.js index 091e0b46574cc..afcc3ec27150b 100644 --- a/workspaces/arborist/lib/arborist/index.js +++ b/workspaces/arborist/lib/arborist/index.js @@ -42,6 +42,7 @@ const mixins = [ require('./load-virtual.js'), require('./rebuild.js'), require('./reify.js'), + require('./isolated-reifier.js'), ] const _workspacesEnabled = Symbol.for('workspacesEnabled') diff --git a/workspaces/arborist/lib/arborist/isolated-reifier.js b/workspaces/arborist/lib/arborist/isolated-reifier.js new file mode 100644 index 0000000000000..f4f1bb8e44362 --- /dev/null +++ b/workspaces/arborist/lib/arborist/isolated-reifier.js @@ -0,0 +1,453 @@ +const _makeIdealGraph = Symbol('makeIdealGraph') +const _createIsolatedTree = Symbol.for('createIsolatedTree') +const _createBundledTree = Symbol('createBundledTree') +const fs = require('fs') +const pacote = require('pacote') +const { join } = require('path') +const { depth } = require('treeverse') +const crypto = require('crypto') + +// cache complicated function results +const memoize = (fn) => { + const memo = new Map() + return async function (arg) { + const key = arg + if (memo.has(key)) { + return memo.get(key) + } + const result = {} + memo.set(key, result) + await fn(result, arg) + return result + } +} + +module.exports = cls => class IsolatedReifier extends cls { + /** + * Create an ideal graph. + * + * An implementation of npm RFC-0042 + * https://github.com/npm/rfcs/blob/main/accepted/0042-isolated-mode.md + * + * This entire file should be considered technical debt that will be resolved + * with an Arborist refactor or rewrite. Embedded logic in Nodes and Links, + * and the incremental state of building trees and reifying contains too many + * assumptions to do a linked mode properly. + * + * Instead, this approach takes a tree built from build-ideal-tree, and + * returns a new tree-like structure without the embedded logic of Node and + * Link classes. + * + * Since the RFC requires leaving the package-lock in place, this approach + * temporarily replaces the tree state for a couple of steps of reifying. + * + **/ + async [_makeIdealGraph] (options) { + /* Make sure that the ideal tree is build as the rest of + * the algorithm depends on it. + */ + const bitOpt = { + ...options, + complete: false, + } + await this.buildIdealTree(bitOpt) + const idealTree = this.idealTree + + this.rootNode = {} + const root = this.rootNode + this.counter = 0 + + // memoize to cache generating proxy Nodes + this.externalProxyMemo = memoize(this.externalProxy.bind(this)) + this.workspaceProxyMemo = memoize(this.workspaceProxy.bind(this)) + + root.external = [] + root.isProjectRoot = true + root.localLocation = idealTree.location + root.localPath = idealTree.path + root.workspaces = await Promise.all( + Array.from(idealTree.fsChildren.values(), this.workspaceProxyMemo)) + const processed = new Set() + const queue = [idealTree, ...idealTree.fsChildren] + while (queue.length !== 0) { + const next = queue.pop() + if (processed.has(next.location)) { + continue + } + processed.add(next.location) + next.edgesOut.forEach(e => { + if (!e.to || (next.package.bundleDependencies || next.package.bundledDependencies || []).includes(e.to.name)) { + return + } + queue.push(e.to) + }) + if (!next.isProjectRoot && !next.isWorkspace) { + root.external.push(await this.externalProxyMemo(next)) + } + } + + await this.assignCommonProperties(idealTree, root) + + this.idealGraph = root + } + + async workspaceProxy (result, node) { + result.localLocation = node.location + result.localPath = node.path + result.isWorkspace = true + result.resolved = node.resolved + await this.assignCommonProperties(node, result) + } + + async externalProxy (result, node) { + await this.assignCommonProperties(node, result) + if (node.hasShrinkwrap) { + const dir = join( + node.root.path, + 'node_modules', + '.store', + `${node.name}@${node.version}` + ) + fs.mkdirSync(dir, { recursive: true }) + // TODO this approach feels wrong + // and shouldn't be necessary for shrinkwraps + await pacote.extract(node.resolved, dir, { + ...this.options, + resolved: node.resolved, + integrity: node.integrity, + }) + const Arborist = this.constructor + const arb = new Arborist({ ...this.options, path: dir }) + await arb[_makeIdealGraph]({ dev: false }) + this.rootNode.external.push(...arb.idealGraph.external) + arb.idealGraph.external.forEach(e => { + e.root = this.rootNode + e.id = `${node.id}=>${e.id}` + }) + result.localDependencies = [] + result.externalDependencies = arb.idealGraph.externalDependencies + result.externalOptionalDependencies = arb.idealGraph.externalOptionalDependencies + result.dependencies = [ + ...result.externalDependencies, + ...result.localDependencies, + ...result.externalOptionalDependencies, + ] + } + result.optional = node.optional + result.resolved = node.resolved + result.version = node.version + } + + async assignCommonProperties (node, result) { + function validEdgesOut (node) { + return [...node.edgesOut.values()].filter(e => e.to && e.to.target && !(node.package.bundledDepenedencies || node.package.bundleDependencies || []).includes(e.to.name)) + } + const edges = validEdgesOut(node) + const optionalDeps = edges.filter(e => e.optional).map(e => e.to.target) + const nonOptionalDeps = edges.filter(e => !e.optional).map(e => e.to.target) + + result.localDependencies = await Promise.all(nonOptionalDeps.filter(n => n.isWorkspace).map(this.workspaceProxyMemo)) + result.externalDependencies = await Promise.all(nonOptionalDeps.filter(n => !n.isWorkspace).map(this.externalProxyMemo)) + result.externalOptionalDependencies = await Promise.all(optionalDeps.map(this.externalProxyMemo)) + result.dependencies = [ + ...result.externalDependencies, + ...result.localDependencies, + ...result.externalOptionalDependencies, + ] + result.root = this.rootNode + result.id = this.counter++ + result.name = node.name + result.package = { ...node.package } + result.package.bundleDependencies = undefined + result.hasInstallScript = node.hasInstallScript + } + + async [_createBundledTree] () { + // TODO: make sure that idealTree object exists + const idealTree = this.idealTree + // TODO: test workspaces having bundled deps + const queue = [] + + for (const [, edge] of idealTree.edgesOut) { + if (edge.to && (idealTree.package.bundleDependencies || idealTree.package.bundledDependencies || []).includes(edge.to.name)) { + queue.push({ from: idealTree, to: edge.to }) + } + } + for (const child of idealTree.fsChildren) { + for (const [, edge] of child.edgesOut) { + if (edge.to && (child.package.bundleDependencies || child.package.bundledDependencies || []).includes(edge.to.name)) { + queue.push({ from: child, to: edge.to }) + } + } + } + + const processed = new Set() + const nodes = new Map() + const edges = [] + while (queue.length !== 0) { + const nextEdge = queue.pop() + const key = `${nextEdge.from.location}=>${nextEdge.to.location}` + // should be impossible, unless bundled is duped + /* istanbul ignore next */ + if (processed.has(key)) { + continue + } + processed.add(key) + const from = nextEdge.from + if (!from.isRoot && !from.isWorkspace) { + nodes.set(from.location, { location: from.location, resolved: from.resolved, name: from.name, optional: from.optional, pkg: { ...from.package, bundleDependencies: undefined } }) + } + const to = nextEdge.to + nodes.set(to.location, { location: to.location, resolved: to.resolved, name: to.name, optional: to.optional, pkg: { ...to.package, bundleDependencies: undefined } }) + edges.push({ from: from.isRoot ? 'root' : from.location, to: to.location }) + + to.edgesOut.forEach(e => { + // an edge out should always have a to + /* istanbul ignore else */ + if (e.to) { + queue.push({ from: e.from, to: e.to }) + } + }) + } + return { edges, nodes } + } + + async [_createIsolatedTree] (idealTree) { + await this[_makeIdealGraph](this.options) + + const proxiedIdealTree = this.idealGraph + + const bundledTree = await this[_createBundledTree]() + + const treeHash = (startNode) => { + // generate short hash based on the dependency tree + // starting at this node + const deps = [] + const branch = [] + depth({ + tree: startNode, + getChildren: node => node.dependencies, + filter: node => node, + visit: node => { + branch.push(`${node.name}@${node.version}`) + deps.push(`${branch.join('->')}::${node.resolved}`) + }, + leave: () => { + branch.pop() + }, + }) + deps.sort() + return crypto.createHash('shake256', { outputLength: 16 }) + .update(deps.join(',')) + .digest('base64') + // Node v14 doesn't support base64url + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/m, '') + } + + const getKey = (idealTreeNode) => { + return `${idealTreeNode.name}@${idealTreeNode.version}-${treeHash(idealTreeNode)}` + } + + const root = { + fsChildren: [], + integrity: null, + inventory: new Map(), + isLink: false, + isRoot: true, + binPaths: [], + edgesIn: new Set(), + edgesOut: new Map(), + hasShrinkwrap: false, + parent: null, + // TODO: we should probably not reference this.idealTree + resolved: this.idealTree.resolved, + isTop: true, + path: proxiedIdealTree.root.localPath, + realpath: proxiedIdealTree.root.localPath, + package: proxiedIdealTree.root.package, + meta: { loadedFromDisk: false }, + global: false, + isProjectRoot: true, + children: [], + } + // root.inventory.set('', t) + // root.meta = this.idealTree.meta + // TODO We should mock better the inventory object because it is used by audit-report.js ... maybe + root.inventory.query = () => { + return [] + } + const processed = new Set() + proxiedIdealTree.workspaces.forEach(c => { + const workspace = { + edgesIn: new Set(), + edgesOut: new Map(), + children: [], + hasInstallScript: c.hasInstallScript, + binPaths: [], + package: c.package, + location: c.localLocation, + path: c.localPath, + realpath: c.localPath, + resolved: c.resolved, + } + root.fsChildren.push(workspace) + root.inventory.set(workspace.location, workspace) + }) + const generateChild = (node, location, pkg, inStore) => { + const newChild = { + global: false, + globalTop: false, + isProjectRoot: false, + isTop: false, + location, + name: node.name, + optional: node.optional, + top: { path: proxiedIdealTree.root.localPath }, + children: [], + edgesIn: new Set(), + edgesOut: new Map(), + binPaths: [], + fsChildren: [], + /* istanbul ignore next -- emulate Node */ + getBundler () { + return null + }, + hasShrinkwrap: false, + inDepBundle: false, + integrity: null, + isLink: false, + isRoot: false, + isInStore: inStore, + path: join(proxiedIdealTree.root.localPath, location), + realpath: join(proxiedIdealTree.root.localPath, location), + resolved: node.resolved, + version: pkg.version, + package: pkg, + } + newChild.target = newChild + root.children.push(newChild) + root.inventory.set(newChild.location, newChild) + } + proxiedIdealTree.external.forEach(c => { + const key = getKey(c) + if (processed.has(key)) { + return + } + processed.add(key) + const location = join('node_modules', '.store', key, 'node_modules', c.name) + generateChild(c, location, c.package, true) + }) + bundledTree.nodes.forEach(node => { + generateChild(node, node.location, node.pkg, false) + }) + bundledTree.edges.forEach(e => { + const from = e.from === 'root' ? root : root.inventory.get(e.from) + const to = root.inventory.get(e.to) + // Maybe optional should be propagated from the original edge + const edge = { optional: false, from, to } + from.edgesOut.set(to.name, edge) + to.edgesIn.add(edge) + }) + const memo = new Set() + + function processEdges (node, externalEdge) { + externalEdge = !!externalEdge + const key = getKey(node) + if (memo.has(key)) { + return + } + memo.add(key) + + let from, nmFolder + if (externalEdge) { + const fromLocation = join('node_modules', '.store', key, 'node_modules', node.name) + from = root.children.find(c => c.location === fromLocation) + nmFolder = join('node_modules', '.store', key, 'node_modules') + } else { + from = node.isProjectRoot ? root : root.fsChildren.find(c => c.location === node.localLocation) + nmFolder = join(node.localLocation, 'node_modules') + } + + const processDeps = (dep, optional, external) => { + optional = !!optional + external = !!external + + const location = join(nmFolder, dep.name) + const binNames = dep.package.bin && Object.keys(dep.package.bin) || [] + const toKey = getKey(dep) + + let target + if (external) { + const toLocation = join('node_modules', '.store', toKey, 'node_modules', dep.name) + target = root.children.find(c => c.location === toLocation) + } else { + target = root.fsChildren.find(c => c.location === dep.localLocation) + } + // TODO: we should no-op is an edge has already been created with the same fromKey and toKey + + binNames.forEach(bn => { + target.binPaths.push(join(from.realpath, 'node_modules', '.bin', bn)) + }) + + const link = { + global: false, + globalTop: false, + isProjectRoot: false, + edgesIn: new Set(), + edgesOut: new Map(), + binPaths: [], + isTop: false, + optional, + location: location, + path: join(dep.root.localPath, nmFolder, dep.name), + realpath: target.path, + name: toKey, + resolved: dep.resolved, + top: { path: dep.root.localPath }, + children: [], + fsChildren: [], + isLink: true, + isStoreLink: true, + isRoot: false, + package: { _id: 'abc', bundleDependencies: undefined, deprecated: undefined, bin: target.package.bin, scripts: dep.package.scripts }, + target, + } + const newEdge1 = { optional, from, to: link } + from.edgesOut.set(dep.name, newEdge1) + link.edgesIn.add(newEdge1) + const newEdge2 = { optional: false, from: link, to: target } + link.edgesOut.set(dep.name, newEdge2) + target.edgesIn.add(newEdge2) + root.children.push(link) + } + + for (const dep of node.localDependencies) { + processEdges(dep, false) + // nonOptional, local + processDeps(dep, false, false) + } + for (const dep of node.externalDependencies) { + processEdges(dep, true) + // nonOptional, external + processDeps(dep, false, true) + } + for (const dep of node.externalOptionalDependencies) { + processEdges(dep, true) + // optional, external + processDeps(dep, true, true) + } + } + + processEdges(proxiedIdealTree, false) + for (const node of proxiedIdealTree.workspaces) { + processEdges(node, false) + } + root.children.forEach(c => c.parent = root) + root.children.forEach(c => c.root = root) + root.root = root + root.target = root + return root + } +} diff --git a/workspaces/arborist/lib/arborist/rebuild.js b/workspaces/arborist/lib/arborist/rebuild.js index 6a675320d864b..e8df69e328ce8 100644 --- a/workspaces/arborist/lib/arborist/rebuild.js +++ b/workspaces/arborist/lib/arborist/rebuild.js @@ -89,6 +89,7 @@ module.exports = cls => class Builder extends cls { const { depNodes, linkNodes, + storeNodes, } = this[_retrieveNodesByType](nodes) // build regular deps @@ -99,6 +100,10 @@ module.exports = cls => class Builder extends cls { this[_resetQueues]() await this[_build](linkNodes, { type: 'links' }) } + if (storeNodes.size) { + this[_resetQueues]() + await this[_build](storeNodes, { type: 'storelinks' }) + } process.emit('timeEnd', 'build') } @@ -130,9 +135,12 @@ module.exports = cls => class Builder extends cls { [_retrieveNodesByType] (nodes) { const depNodes = new Set() const linkNodes = new Set() + const storeNodes = new Set() for (const node of nodes) { - if (node.isLink) { + if (node.isStoreLink) { + storeNodes.add(node) + } else if (node.isLink) { linkNodes.add(node) } else { depNodes.add(node) @@ -154,6 +162,7 @@ module.exports = cls => class Builder extends cls { return { depNodes, linkNodes, + storeNodes, } } diff --git a/workspaces/arborist/lib/arborist/reify.js b/workspaces/arborist/lib/arborist/reify.js index e5ccec5c71d96..3c8059d281d13 100644 --- a/workspaces/arborist/lib/arborist/reify.js +++ b/workspaces/arborist/lib/arborist/reify.js @@ -1,5 +1,4 @@ // mixin implementing the reify method - const onExit = require('../signal-handling.js') const pacote = require('pacote') const AuditReport = require('../audit-report.js') @@ -10,8 +9,9 @@ const debug = require('../debug.js') const walkUp = require('walk-up-path') const log = require('proc-log') const hgi = require('hosted-git-info') +const rpj = require('read-package-json-fast') -const { dirname, resolve, relative } = require('path') +const { dirname, resolve, relative, join } = require('path') const { depth: dfwalk } = require('treeverse') const { lstat, @@ -106,6 +106,8 @@ const _resolvedAdd = Symbol.for('resolvedAdd') const _usePackageLock = Symbol.for('usePackageLock') const _formatPackageLock = Symbol.for('formatPackageLock') +const _createIsolatedTree = Symbol.for('createIsolatedTree') + module.exports = cls => class Reifier extends cls { constructor (options) { super(options) @@ -138,6 +140,8 @@ module.exports = cls => class Reifier extends cls { // public method async reify (options = {}) { + const linked = (options.installStrategy || this.options.installStrategy) === 'linked' + if (this[_packageLockOnly] && this[_global]) { const er = new Error('cannot generate lockfile for global packages') er.code = 'ESHRINKWRAPGLOBAL' @@ -154,8 +158,22 @@ module.exports = cls => class Reifier extends cls { process.emit('time', 'reify') await this[_validatePath]() await this[_loadTrees](options) + + const oldTree = this.idealTree + if (linked) { + // swap out the tree with the isolated tree + // this is currently technical debt which will be resolved in a refactor + // of Node/Link trees + log.warn('reify', 'The "linked" install strategy is EXPERIMENTAL and may contain bugs.') + this.idealTree = await this[_createIsolatedTree](this.idealTree) + } await this[_diffTrees]() await this[_reifyPackages]() + if (linked) { + // swap back in the idealTree + // so that the lockfile is preserved + this.idealTree = oldTree + } await this[_saveIdealTree](options) await this[_copyIdealToActual]() // This is a very bad pattern and I can't wait to stop doing it @@ -634,44 +652,40 @@ module.exports = cls => class Reifier extends cls { } async [_extractOrLink] (node) { - // in normal cases, node.resolved should *always* be set by now. - // however, it is possible when a lockfile is damaged, or very old, - // or in some other race condition bugs in npm v6, that a previously - // bundled dependency will have just a version, but no resolved value, - // and no 'bundled: true' setting. - // Do the best with what we have, or else remove it from the tree - // entirely, since we can't possibly reify it. - let res = null - if (node.resolved) { - const registryResolved = this[_registryResolved](node.resolved) - if (registryResolved) { - res = `${node.name}@${registryResolved}` - } - } else if (node.packageName && node.version) { - res = `${node.packageName}@${node.version}` - } - - // no idea what this thing is. remove it from the tree. - if (!res) { - const warning = 'invalid or damaged lockfile detected\n' + - 'please re-try this operation once it completes\n' + - 'so that the damage can be corrected, or perform\n' + - 'a fresh install with no lockfile if the problem persists.' - log.warn('reify', warning) - log.verbose('reify', 'unrecognized node in tree', node.path) - node.parent = null - node.fsParent = null - this[_addNodeToTrashList](node) - return - } - const nm = resolve(node.parent.path, 'node_modules') await this[_validateNodeModules](nm) - if (node.isLink) { - await rm(node.path, { recursive: true, force: true }) - await this[_symlink](node) - } else { + if (!node.isLink) { + // in normal cases, node.resolved should *always* be set by now. + // however, it is possible when a lockfile is damaged, or very old, + // or in some other race condition bugs in npm v6, that a previously + // bundled dependency will have just a version, but no resolved value, + // and no 'bundled: true' setting. + // Do the best with what we have, or else remove it from the tree + // entirely, since we can't possibly reify it. + let res = null + if (node.resolved) { + const registryResolved = this[_registryResolved](node.resolved) + if (registryResolved) { + res = `${node.name}@${registryResolved}` + } + } else if (node.package.name && node.version) { + res = `${node.package.name}@${node.version}` + } + + // no idea what this thing is. remove it from the tree. + if (!res) { + const warning = 'invalid or damaged lockfile detected\n' + + 'please re-try this operation once it completes\n' + + 'so that the damage can be corrected, or perform\n' + + 'a fresh install with no lockfile if the problem persists.' + log.warn('reify', warning) + log.verbose('reify', 'unrecognized node in tree', node.path) + node.parent = null + node.fsParent = null + this[_addNodeToTrashList](node) + return + } await debug(async () => { const st = await lstat(node.path).catch(e => null) if (st && !st.isDirectory()) { @@ -688,7 +702,17 @@ module.exports = cls => class Reifier extends cls { resolved: node.resolved, integrity: node.integrity, }) + // store nodes don't use Node class so node.package doesn't get updated + if (node.isInStore) { + const pkg = await rpj(join(node.path, 'package.json')) + node.package.scripts = pkg.scripts + } + return } + + // node.isLink + await rm(node.path, { recursive: true, force: true }) + await this[_symlink](node) } async [_symlink] (node) { diff --git a/workspaces/arborist/lib/link.js b/workspaces/arborist/lib/link.js index ebdbc94285f1c..197f96c5c2ddb 100644 --- a/workspaces/arborist/lib/link.js +++ b/workspaces/arborist/lib/link.js @@ -8,7 +8,7 @@ const _delistFromMeta = Symbol.for('_delistFromMeta') const _refreshLocation = Symbol.for('_refreshLocation') class Link extends Node { constructor (options) { - const { root, realpath, target, parent, fsParent } = options + const { root, realpath, target, parent, fsParent, isStoreLink } = options if (!realpath && !(target && target.path)) { throw new TypeError('must provide realpath for Link node') @@ -23,6 +23,8 @@ class Link extends Node { : null), }) + this.isStoreLink = isStoreLink || false + if (target) { this.target = target } else if (this.realpath === this.root.path) { diff --git a/workspaces/arborist/lib/node.js b/workspaces/arborist/lib/node.js index b90a2acf8f8ae..b21a3d8e3de0a 100644 --- a/workspaces/arborist/lib/node.js +++ b/workspaces/arborist/lib/node.js @@ -91,6 +91,7 @@ class Node { installLinks = false, legacyPeerDeps = false, linksIn, + isInStore = false, hasShrinkwrap, overrides, loadOverrides = false, @@ -113,6 +114,7 @@ class Node { this[_workspaces] = null this.errors = error ? [error] : [] + this.isInStore = isInStore // this will usually be null, except when modeling a // package's dependencies in a virtual root. diff --git a/workspaces/arborist/package.json b/workspaces/arborist/package.json index a7e8132123fba..a2f85102a9c5a 100644 --- a/workspaces/arborist/package.json +++ b/workspaces/arborist/package.json @@ -45,10 +45,12 @@ "minify-registry-metadata": "^3.0.0", "nock": "^13.2.0", "tap": "^16.3.2", + "tar-stream": "^3.0.0", "tcompare": "^5.0.6" }, "scripts": { "test": "tap", + "test-only": "tap --only", "posttest": "node ../.. run lint", "snap": "tap", "postsnap": "npm run lintfix", diff --git a/workspaces/arborist/tap-snapshots/test/link.js.test.cjs b/workspaces/arborist/tap-snapshots/test/link.js.test.cjs index adffc182b23b0..7c921fbbd72bc 100644 --- a/workspaces/arborist/tap-snapshots/test/link.js.test.cjs +++ b/workspaces/arborist/tap-snapshots/test/link.js.test.cjs @@ -19,6 +19,8 @@ Link { "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "../../../../../some/other/path", @@ -62,6 +64,7 @@ exports[`test/link.js TAP > instantiate without providing target 1`] = ` "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set { <*ref_1>, @@ -78,6 +81,8 @@ exports[`test/link.js TAP > instantiate without providing target 1`] = ` "tops": Set {}, }, }, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "../../../../../some/other/path", @@ -103,6 +108,7 @@ exports[`test/link.js TAP > instantiate without providing target 1`] = ` "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set { <*ref_1>, diff --git a/workspaces/arborist/tap-snapshots/test/node.js.test.cjs b/workspaces/arborist/tap-snapshots/test/node.js.test.cjs index bc571fc9ae443..93f1f46628252 100644 --- a/workspaces/arborist/tap-snapshots/test/node.js.test.cjs +++ b/workspaces/arborist/tap-snapshots/test/node.js.test.cjs @@ -22,6 +22,7 @@ exports[`test/node.js TAP basic instantiation > just a lone root node 1`] = ` "inventory": Inventory { "" => <*ref_1>, }, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "", @@ -209,6 +210,8 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/foo", @@ -234,6 +237,8 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/unknown", @@ -276,6 +281,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set { Link { @@ -295,6 +301,8 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/foo", @@ -333,6 +341,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set { Link { @@ -348,6 +357,8 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/unknown", @@ -395,6 +406,8 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/foo", @@ -421,6 +434,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set { Link { @@ -440,6 +454,8 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/foo", @@ -477,6 +493,8 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/unknown", @@ -503,6 +521,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set { Link { @@ -518,6 +537,8 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/unknown", @@ -543,6 +564,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "tops": Set {}, }, }, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "", @@ -569,6 +591,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set { Link { @@ -588,6 +611,8 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/foo", @@ -626,6 +651,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set { Link { @@ -641,6 +667,8 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/unknown", @@ -689,6 +717,8 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/node_modules/meta/node_modules/metameta", @@ -725,6 +755,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": "newMeta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/node_modules/meta", @@ -776,6 +807,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -794,6 +826,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": "prod", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod", @@ -832,6 +865,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": "bundled", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/bundled", @@ -863,6 +897,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": "dev", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/dev", @@ -894,6 +929,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": "opt", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/optional", @@ -925,6 +961,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": "peer", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/peer", @@ -952,6 +989,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": "extraneous", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/extraneous", @@ -987,6 +1025,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": "meta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set { Link { @@ -1002,6 +1041,8 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/node_modules/meta/node_modules/metameta", @@ -1079,6 +1120,8 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/node_modules/meta/node_modules/metameta", @@ -1115,6 +1158,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": "newMeta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/node_modules/meta", @@ -1166,6 +1210,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -1184,6 +1229,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": "prod", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod", @@ -1215,6 +1261,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -1253,6 +1300,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": "bundled", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/bundled", @@ -1284,6 +1332,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": "dev", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/dev", @@ -1315,6 +1364,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": "opt", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/optional", @@ -1346,6 +1396,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": "peer", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/peer", @@ -1373,6 +1424,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": "extraneous", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/extraneous", @@ -1408,6 +1460,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": "meta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set { Link { @@ -1423,6 +1476,8 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/node_modules/meta/node_modules/metameta", @@ -1462,6 +1517,8 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/node_modules/meta/node_modules/metameta", @@ -1498,6 +1555,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": "newMeta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/node_modules/meta", @@ -1524,6 +1582,8 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/node_modules/meta/node_modules/metameta", @@ -1537,6 +1597,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "tops": Set {}, }, }, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "", @@ -1567,6 +1628,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -1614,6 +1676,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "installLinks": false, "integrity": "meta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/node_modules/meta", @@ -1665,6 +1728,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -1683,6 +1747,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "installLinks": false, "integrity": "prod", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod", @@ -1721,6 +1786,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "installLinks": false, "integrity": "bundled", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/bundled", @@ -1752,6 +1818,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "installLinks": false, "integrity": "dev", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/dev", @@ -1783,6 +1850,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "installLinks": false, "integrity": "opt", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/optional", @@ -1814,6 +1882,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "installLinks": false, "integrity": "peer", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/peer", @@ -1841,6 +1910,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "installLinks": false, "integrity": "extraneous", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/extraneous", @@ -1917,6 +1987,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "installLinks": false, "integrity": "meta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/node_modules/meta", @@ -1968,6 +2039,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -1986,6 +2058,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "installLinks": false, "integrity": "prod", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod", @@ -2017,6 +2090,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -2055,6 +2129,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "installLinks": false, "integrity": "meta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/node_modules/meta", @@ -2093,6 +2168,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "installLinks": false, "integrity": "bundled", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/bundled", @@ -2124,6 +2200,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "installLinks": false, "integrity": "dev", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/dev", @@ -2155,6 +2232,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "installLinks": false, "integrity": "opt", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/optional", @@ -2186,6 +2264,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "installLinks": false, "integrity": "peer", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/peer", @@ -2213,6 +2292,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "installLinks": false, "integrity": "extraneous", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/extraneous", @@ -2227,6 +2307,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "tops": Set {}, }, }, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "", @@ -2257,6 +2338,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -2316,6 +2398,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -2334,6 +2417,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "installLinks": false, "integrity": "prod", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod", @@ -2372,6 +2456,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "installLinks": false, "integrity": "bundled", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/bundled", @@ -2403,6 +2488,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "installLinks": false, "integrity": "dev", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/dev", @@ -2434,6 +2520,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "installLinks": false, "integrity": "opt", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/optional", @@ -2465,6 +2552,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "installLinks": false, "integrity": "peer", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/peer", @@ -2492,6 +2580,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "installLinks": false, "integrity": "extraneous", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/extraneous", @@ -2533,6 +2622,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "installLinks": false, "integrity": "meta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/meta", @@ -2621,6 +2711,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -2639,6 +2730,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "installLinks": false, "integrity": "prod", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod", @@ -2670,6 +2762,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -2708,6 +2801,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "installLinks": false, "integrity": "bundled", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/bundled", @@ -2739,6 +2833,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "installLinks": false, "integrity": "dev", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/dev", @@ -2770,6 +2865,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "installLinks": false, "integrity": "opt", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/optional", @@ -2801,6 +2897,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "installLinks": false, "integrity": "peer", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/peer", @@ -2828,6 +2925,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "installLinks": false, "integrity": "extraneous", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/extraneous", @@ -2869,6 +2967,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "installLinks": false, "integrity": "meta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/meta", @@ -2883,6 +2982,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "tops": Set {}, }, }, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "", @@ -2913,6 +3013,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -2972,6 +3073,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -2990,6 +3092,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "prod", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod", @@ -3025,6 +3128,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "bundled", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/bundled", @@ -3056,6 +3160,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "dev", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/dev", @@ -3087,6 +3192,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "opt", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/optional", @@ -3118,6 +3224,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "peer", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/peer", @@ -3145,6 +3252,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "extraneous", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/extraneous", @@ -3173,6 +3281,8 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/meta/node_modules/metameta", @@ -3212,6 +3322,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "newMeta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set { Link { @@ -3227,6 +3338,8 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/meta/node_modules/metameta", @@ -3326,6 +3439,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -3344,6 +3458,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "prod", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod", @@ -3375,6 +3490,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -3410,6 +3526,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "bundled", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/bundled", @@ -3441,6 +3558,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "dev", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/dev", @@ -3472,6 +3590,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "opt", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/optional", @@ -3503,6 +3622,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "peer", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/peer", @@ -3530,6 +3650,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "extraneous", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/extraneous", @@ -3556,6 +3677,8 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/meta/node_modules/metameta", @@ -3583,6 +3706,8 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/meta/node_modules/metameta", @@ -3622,6 +3747,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "newMeta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set { Link { @@ -3637,6 +3763,8 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/meta/node_modules/metameta", @@ -3662,6 +3790,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "tops": Set {}, }, }, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "", @@ -3692,6 +3821,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -3751,6 +3881,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -3769,6 +3900,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "prod", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod", @@ -3804,6 +3936,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "bundled", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/bundled", @@ -3835,6 +3968,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "dev", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/dev", @@ -3866,6 +4000,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "opt", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/optional", @@ -3897,6 +4032,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "peer", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/peer", @@ -3924,6 +4060,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "extraneous", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/extraneous", @@ -3952,6 +4089,8 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/meta/node_modules/metameta", @@ -3991,6 +4130,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "newMeta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set { Link { @@ -4006,6 +4146,8 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/meta/node_modules/metameta", @@ -4105,6 +4247,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -4123,6 +4266,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "prod", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod", @@ -4154,6 +4298,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -4189,6 +4334,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "bundled", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/bundled", @@ -4220,6 +4366,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "dev", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/dev", @@ -4251,6 +4398,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "opt", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/optional", @@ -4282,6 +4430,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "peer", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/peer", @@ -4309,6 +4458,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "extraneous", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/extraneous", @@ -4335,6 +4485,8 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/meta/node_modules/metameta", @@ -4362,6 +4514,8 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/meta/node_modules/metameta", @@ -4401,6 +4555,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "newMeta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set { Link { @@ -4416,6 +4571,8 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/meta/node_modules/metameta", @@ -4441,6 +4598,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "tops": Set {}, }, }, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "", @@ -4471,6 +4629,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -4508,6 +4667,8 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/node_modules/meta/node_modules/metameta", @@ -4544,6 +4705,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": "newMeta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/node_modules/meta", @@ -4595,6 +4757,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -4613,6 +4776,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": "prod", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod", @@ -4651,6 +4815,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": "bundled", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/bundled", @@ -4682,6 +4847,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": "dev", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/dev", @@ -4713,6 +4879,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": "opt", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/optional", @@ -4744,6 +4911,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": "peer", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/peer", @@ -4771,6 +4939,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": "extraneous", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/extraneous", @@ -4806,6 +4975,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": "meta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set { Link { @@ -4821,6 +4991,8 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/node_modules/meta/node_modules/metameta", @@ -4898,6 +5070,8 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/node_modules/meta/node_modules/metameta", @@ -4934,6 +5108,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": "newMeta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/node_modules/meta", @@ -4985,6 +5160,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -5003,6 +5179,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": "prod", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod", @@ -5034,6 +5211,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -5072,6 +5250,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": "bundled", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/bundled", @@ -5103,6 +5282,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": "dev", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/dev", @@ -5134,6 +5314,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": "opt", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/optional", @@ -5165,6 +5346,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": "peer", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/peer", @@ -5192,6 +5374,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": "extraneous", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/extraneous", @@ -5227,6 +5410,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": "meta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set { Link { @@ -5242,6 +5426,8 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/node_modules/meta/node_modules/metameta", @@ -5281,6 +5467,8 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/node_modules/meta/node_modules/metameta", @@ -5317,6 +5505,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": "newMeta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/node_modules/meta", @@ -5343,6 +5532,8 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/node_modules/meta/node_modules/metameta", @@ -5356,6 +5547,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "tops": Set {}, }, }, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "", @@ -5386,6 +5578,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -5433,6 +5626,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "installLinks": false, "integrity": "meta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/node_modules/meta", @@ -5484,6 +5678,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -5502,6 +5697,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "installLinks": false, "integrity": "prod", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod", @@ -5540,6 +5736,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "installLinks": false, "integrity": "bundled", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/bundled", @@ -5571,6 +5768,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "installLinks": false, "integrity": "dev", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/dev", @@ -5602,6 +5800,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "installLinks": false, "integrity": "opt", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/optional", @@ -5633,6 +5832,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "installLinks": false, "integrity": "peer", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/peer", @@ -5660,6 +5860,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "installLinks": false, "integrity": "extraneous", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/extraneous", @@ -5736,6 +5937,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "installLinks": false, "integrity": "meta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/node_modules/meta", @@ -5787,6 +5989,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -5805,6 +6008,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "installLinks": false, "integrity": "prod", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod", @@ -5836,6 +6040,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -5874,6 +6079,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "installLinks": false, "integrity": "meta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/node_modules/meta", @@ -5912,6 +6118,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "installLinks": false, "integrity": "bundled", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/bundled", @@ -5943,6 +6150,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "installLinks": false, "integrity": "dev", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/dev", @@ -5974,6 +6182,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "installLinks": false, "integrity": "opt", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/optional", @@ -6005,6 +6214,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "installLinks": false, "integrity": "peer", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/peer", @@ -6032,6 +6242,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "installLinks": false, "integrity": "extraneous", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/extraneous", @@ -6046,6 +6257,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "tops": Set {}, }, }, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "", @@ -6076,6 +6288,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -6135,6 +6348,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -6153,6 +6367,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "installLinks": false, "integrity": "prod", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod", @@ -6191,6 +6406,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "installLinks": false, "integrity": "bundled", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/bundled", @@ -6222,6 +6438,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "installLinks": false, "integrity": "dev", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/dev", @@ -6253,6 +6470,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "installLinks": false, "integrity": "opt", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/optional", @@ -6284,6 +6502,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "installLinks": false, "integrity": "peer", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/peer", @@ -6311,6 +6530,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "installLinks": false, "integrity": "extraneous", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/extraneous", @@ -6352,6 +6572,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "installLinks": false, "integrity": "meta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/meta", @@ -6440,6 +6661,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -6458,6 +6680,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "installLinks": false, "integrity": "prod", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod", @@ -6489,6 +6712,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -6527,6 +6751,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "installLinks": false, "integrity": "bundled", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/bundled", @@ -6558,6 +6783,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "installLinks": false, "integrity": "dev", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/dev", @@ -6589,6 +6815,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "installLinks": false, "integrity": "opt", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/optional", @@ -6620,6 +6847,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "installLinks": false, "integrity": "peer", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/peer", @@ -6647,6 +6875,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "installLinks": false, "integrity": "extraneous", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/extraneous", @@ -6688,6 +6917,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "installLinks": false, "integrity": "meta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/meta", @@ -6702,6 +6932,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "tops": Set {}, }, }, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "", @@ -6732,6 +6963,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -6791,6 +7023,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -6809,6 +7042,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "prod", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod", @@ -6844,6 +7078,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "bundled", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/bundled", @@ -6875,6 +7110,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "dev", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/dev", @@ -6906,6 +7142,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "opt", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/optional", @@ -6937,6 +7174,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "peer", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/peer", @@ -6964,6 +7202,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "extraneous", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/extraneous", @@ -6992,6 +7231,8 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/meta/node_modules/metameta", @@ -7031,6 +7272,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "newMeta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set { Link { @@ -7046,6 +7288,8 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/meta/node_modules/metameta", @@ -7145,6 +7389,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -7163,6 +7408,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "prod", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod", @@ -7194,6 +7440,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -7229,6 +7476,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "bundled", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/bundled", @@ -7260,6 +7508,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "dev", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/dev", @@ -7291,6 +7540,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "opt", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/optional", @@ -7322,6 +7572,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "peer", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/peer", @@ -7349,6 +7600,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "extraneous", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/extraneous", @@ -7375,6 +7627,8 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/meta/node_modules/metameta", @@ -7402,6 +7656,8 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/meta/node_modules/metameta", @@ -7441,6 +7697,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "newMeta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set { Link { @@ -7456,6 +7713,8 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/meta/node_modules/metameta", @@ -7481,6 +7740,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "tops": Set {}, }, }, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "", @@ -7511,6 +7771,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -7570,6 +7831,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -7588,6 +7850,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "prod", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod", @@ -7623,6 +7886,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "bundled", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/bundled", @@ -7654,6 +7918,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "dev", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/dev", @@ -7685,6 +7950,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "opt", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/optional", @@ -7716,6 +7982,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "peer", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/peer", @@ -7743,6 +8010,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "extraneous", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/extraneous", @@ -7771,6 +8039,8 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/meta/node_modules/metameta", @@ -7810,6 +8080,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "newMeta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set { Link { @@ -7825,6 +8096,8 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/meta/node_modules/metameta", @@ -7924,6 +8197,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -7942,6 +8216,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "prod", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod", @@ -7973,6 +8248,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", @@ -8008,6 +8284,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "bundled", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/bundled", @@ -8039,6 +8316,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "dev", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/dev", @@ -8070,6 +8348,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "opt", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/optional", @@ -8101,6 +8380,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "peer", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/peer", @@ -8128,6 +8408,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "extraneous", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/extraneous", @@ -8154,6 +8435,8 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/meta/node_modules/metameta", @@ -8181,6 +8464,8 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/meta/node_modules/metameta", @@ -8220,6 +8505,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "newMeta", "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set { Link { @@ -8235,6 +8521,8 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": "metameta", "inventory": Inventory {}, + "isInStore": false, + "isStoreLink": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/meta/node_modules/metameta", @@ -8260,6 +8548,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "tops": Set {}, }, }, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "", @@ -8290,6 +8579,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "installLinks": false, "integrity": null, "inventory": Inventory {}, + "isInStore": false, "legacyPeerDeps": false, "linksIn": Set {}, "location": "node_modules/prod/foo", diff --git a/workspaces/arborist/test/arborist/reify.js b/workspaces/arborist/test/arborist/reify.js index 7c250a34d236b..026054e756577 100644 --- a/workspaces/arborist/test/arborist/reify.js +++ b/workspaces/arborist/test/arborist/reify.js @@ -3151,3 +3151,61 @@ t.only('should preserve exact ranges, missing actual tree', async (t) => { await arb.reify() }) }) + +t.test('install stategy linked', async (t) => { + const Arborist = require('../../lib/index.js') + const abbrev = resolve(__dirname, + '../fixtures/registry-mocks/content/abbrev/-/abbrev-1.1.1.tgz') + const abbrevTGZ = fs.readFileSync(abbrev) + + const abbrevPackument = JSON.stringify({ + _id: 'abbrev', + _rev: 'lkjadflkjasdf', + name: 'abbrev', + 'dist-tags': { latest: '1.1.1' }, + versions: { + '1.1.1': { + name: 'abbrev', + version: '1.1.1', + dist: { + tarball: 'https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz', + }, + }, + }, + }) + + t.test('should install package linked', async (t) => { + const testdir = t.testdir({ + project: { + 'package.json': JSON.stringify({ + name: 'myproject', + version: '1.0.0', + dependencies: { + abbrev: '1.1.1', + }, + }), + }, + }) + + tnock(t, 'https://registry.npmjs.org') + .get('/abbrev') + .reply(200, abbrevPackument) + + tnock(t, 'https://registry.npmjs.org') + .get('/abbrev/-/abbrev-1.1.1.tgz') + .reply(200, abbrevTGZ) + + const path = resolve(testdir, 'project') + const arb = new Arborist({ + path, + registry: 'https://registry.npmjs.org', + cache: resolve(testdir, 'cache'), + installStrategy: 'linked', + }) + await arb.reify({ installStrategy: 'linked' }) + const abbrev = fs.lstatSync(resolve(path, 'node_modules', 'abbrev')) + const store = fs.lstatSync(resolve(path, 'node_modules', '.store')) + t.ok(store.isDirectory(), 'abbrev got installed') + t.ok(abbrev.isSymbolicLink(), 'abbrev got installed') + }) +}) diff --git a/workspaces/arborist/test/fixtures/isolated-nock.js b/workspaces/arborist/test/fixtures/isolated-nock.js new file mode 100644 index 0000000000000..5086d3931923c --- /dev/null +++ b/workspaces/arborist/test/fixtures/isolated-nock.js @@ -0,0 +1,203 @@ +const nock = require('nock') +const tar = require('tar-stream') +const Stream = require('stream') +const fs = require('fs') +const path = require('path') +const os = require('os') + +/* Utility to extract a buffer out of a stream */ +class StreamToBuffer extends Stream.Writable { + constructor (opts) { + super(opts) + this._bufferArray = [] + } + + _write (chunk, encoding, callback) { + this._bufferArray.push(chunk) + callback() + } + + _final (callback) { + this.buffer = Buffer.concat(this._bufferArray) + callback() + } +} + +/** + * packPackageToStream + * Uses 'tar-stream' to create the tar stream without touching the file system. + */ +function packPackageToStream (manifest, reg) { + const { + name, + version, + } = manifest + const { shrinkwrap, bundledDeps, ...rest } = manifest + + const pack = tar.pack() + const manifestString = JSON.stringify({ + ...rest, + }) + const index = `console.log('Hello from ${name}@${version}!')` + const unscopedName = name.replace(/^@[^/]*\//, '') + pack.entry({ name: unscopedName, type: 'directory' }) + pack.entry({ name: `${unscopedName}/package.json` }, manifestString) + pack.entry({ name: `${unscopedName}/index.js` }, index) + pack.entry({ name: `${unscopedName}/bin.js` }, '#!/usr/bin/env node\nconsole.log("bin")') + if (shrinkwrap) { + pack.entry({ name: `${unscopedName}/npm-shrinkwrap.json` }, shrinkwrap.replace(/##REG##/g, reg)) + } + if (bundledDeps) { + pack.entry({ name: `${unscopedName}/node_modules`, type: 'directory' }) + bundledDeps.forEach(d => { + pack.entry({ name: `${unscopedName}/node_modules/${d.name}`, type: 'directory' }) + pack.entry({ name: `${unscopedName}/node_modules/${d.name}/package.json` }, JSON.stringify(d)) + pack.entry({ name: `${unscopedName}/node_modules/${d.name}/index.js` }, `console.log('Hello from ${d.name}@${d.version}!')` + ) + }) + } + + pack.finalize() + return pack +} + +/** + * Pack a package for publish. + * Returns a buffer containing a tarball of the package. + */ +async function packPackage (manifest, reg) { + const packStream = packPackageToStream(manifest, reg) + + const tarBuffer = packStream.pipe(new StreamToBuffer()) + + return new Promise((resolve, reject) => { + tarBuffer.on('close', () => { + resolve(tarBuffer.buffer) + }) + tarBuffer.on('error', reject) + }) +} + +/** + * Publish the given package to the given registry. + */ +async function publishPackage (registry, manifest, packuments) { + const { + name, + version, + } = manifest + const { shrinkwrap, bundledDeps, ...rest } = manifest + + if (packuments.has(name)) { + packuments.get(name).versions[version] = { + bin: './bin.js', + ...rest, + dist: { + tarball: `${registry}/${name.replace(/\//g, '-')}/${version}.tar`, + }, + } + } else { + packuments.set(name, { + name, + 'dist-tags': { + latest: version, + }, + versions: { + [version]: { + ...rest, + bin: './bin.js', + dist: { + tarball: `${registry}/${name.replace(/\//g, '-')}/${version}.tar`, + }, + }, + }, + }) + } + + const tarball = await packPackage(manifest, registry) + + nock(registry) + .persist() + .get(`/${name.replace(/\//g, '-')}/${version}.tar`) + .reply(200, tarball) +} + +/** + * Given an object decribing a dependency graph, this funcion will materialize + * this dependency graph on disk and in a registry. + * + * The input has the following shape: + * { + * registry: [ package1, package2,... ], + * root: { name: , version: , dependencies: } + * } + * package# represent a registry package and is of the following shape: + * { name: , version: , dependencies?: } + * + * The return value is of the form { dir, registry } where: + * - dir is the location on disk of the root package + * - registry is the url of the registry which has the necessary packages + * + * Only simple graphs are currenly supported, the following features will come in further iterations: + * - lockfile + * - shinkwrap + * - bundled dependencies + * + * The API of this function is not stable and is likely to evolve as we add more features. + */ +async function getRepo (graph) { + // Generate a new random registry every time to prevent interference between tests + const registry = `https://${Math.random().toString(36).substring(2)}.test` + + const packuments = new Map() + // Publish all the registery packages + await Promise.all(graph.registry.map(o => + publishPackage(registry, o, packuments))) + + packuments.forEach((packument, name) => { + nock(registry) + .persist() + .get(`/${name.replace(/\//g, '%2f')}`) + .reply(200, packument) + }) + + // Generate the root of the graph on disk + const root = graph.root + const workspaces = graph.workspaces || [] + const repo = { + 'package.json': JSON.stringify({ + workspaces: workspaces.length !== 0 ? ['packages/*'] : undefined, + ...root, + }), + packages: {}, + } + workspaces.forEach(wp => { + repo.packages[wp.name] = { + 'package.json': JSON.stringify(wp), + 'bin.js': '#!/usr/bin/env node\nconsole.log("bin")', + 'index.js': `console.log('Hello from workspace ${wp.name}')`, + } + }) + const dir = testdir(repo) + return { dir, registry } +} + +function testdir (structure) { + const dir = fs.mkdtempSync(`${fs.realpathSync(os.tmpdir())}/test-`) + createDir(dir, structure) + return dir +} + +function createDir (dir, structure) { + Object.entries(structure).forEach(([key, value]) => { + if (typeof value === 'object') { + const newDir = path.join(dir, key) + fs.mkdirSync(newDir) + createDir(newDir, value) + } else { + fs.writeFileSync(path.join(dir, key), value) + } + }) +} + +exports.getRepo = getRepo diff --git a/workspaces/arborist/test/isolated-mode.js b/workspaces/arborist/test/isolated-mode.js new file mode 100644 index 0000000000000..eaecd9453754a --- /dev/null +++ b/workspaces/arborist/test/isolated-mode.js @@ -0,0 +1,1574 @@ +const tap = require('tap') +const fs = require('fs') +const path = require('path') +const os = require('os') + +const oldMap = Map + +class newMap extends oldMap { + get (prop) { + const newThis = this.__target || this + return oldMap.prototype.get.bind(newThis)(prop) + } +} +Map = newMap + +const getTempDir = () => fs.realpathSync(os.tmpdir()) + +const Arborist = require('../lib/arborist') +const { getRepo } = require('./fixtures/isolated-nock') + +/** + * The testing framework here is work in progress, in particular it does not have nice ergonomics. + * The syntactic suggar for this framework will be introduced over time as we add more features. + * + * The framework has two parts: + * - Mocking: The tool generates a test repo based on a declarative list of packages. + * - Asserting: Some generic rules are defined which assert a particular contract of a resolved dependency graph. + * For each test we declaratively define the expected resolved dependency graph and apply all the rules to it. + * This validates that arborist produced the expected dependency graph and respect all the contracts set by the rules. + * + * The automatic assertions aims to make new tests easy. + * A rule needs to be written only once and can be asserted against many graphs cheaply. + * The only part that needs to be produced by hand is the conversion from the list of packages to a resolved dependency graph. + * Automating this part would mean reimplementing the full resolution algorithm for the tests, this would be error prone. + * Manually defining declaratively the input and the output of arborist is what gives us confidence that the tests do what + * we want. + * + **/ + +const rule1 = { + description: 'Any package (except root package and workspace) should be able to require itself.', + apply: (t, dir, resolvedGraph, alreadyAsserted) => { + const graph = parseGraph(resolvedGraph) + const allPackages = getAllPackages(withRequireChain(graph)) + allPackages.filter(p => p.chain.length !== 0).forEach(p => { + const resolveChain = [...p.chain, p.name] + const key = p.initialDir + ' => ' + resolveChain.join(' => ') + if (alreadyAsserted.has(key)) { + return + } + alreadyAsserted.add(key) + t.ok(setupRequire(path.join(dir, p.initialDir))(...resolveChain), + `Rule 1: Package "${[p.initialDir.replace('packages/', ''), ...p.chain].join(' => ')}" should have access to itself using its own name.`) + }) + }, +} + +const rule2 = { + description: 'Packages can require their resolved dependencies.', + apply: (t, dir, resolvedGraph, alreadyAsserted) => { + const graph = parseGraph(resolvedGraph) + const allPackages = getAllPackages(withRequireChain(graph)) + allPackages.forEach(p => { + (p.dependencies || []).filter(d => !isLoopToken(d)).map(d => d.name).forEach(n => { + const resolveChain = [...p.chain, n] + const key = p.initialDir + ' => ' + resolveChain.join(' => ') + if (alreadyAsserted.has(key)) { + return + } + alreadyAsserted.add(key) + t.ok(path.join(dir, p.initialDir), + `Rule 2: ${p.chain.length === 0 && p.initialDir === '.' ? 'The root' : `Package "${[p.initialDir.replace('packages/', ''), ...p.chain].join(' => ')}"`} should have access to "${n}" because it has it as a resolved dependency.`) + }) + }) + // testing circular deps + allPackages.forEach(p => { + (p.dependencies || []).filter(d => isLoopToken(d)).forEach(token => { + const back = parseLoopToken(token) + const n = p.chain.slice(-1 - back)[0] // getting the name of the circular dep by going back in the chain + const resolveChain = [...p.chain, n] + const key = p.initialDir + ' => ' + resolveChain.join(' => ') + if (alreadyAsserted.has(key)) { + return + } + alreadyAsserted.add(key) + t.ok(setupRequire(path.join(dir, p.initialDir))(...resolveChain), + `Rule 2: ${p.chain.length === 0 && p.initialDir === '.' ? 'The root' : `Package "${[p.initialDir.replace('packages/', ''), ...p.chain].join(' => ')}"`} should have access to "${n}" because it has it as a resolved dependency.`) + }) + }) + }, +} + +const rule3 = { + description: 'Any package can require a package installed at the root.', + apply: (t, dir, resolvedGraph, alreadyAsserted) => { + const graph = parseGraph(resolvedGraph) + const rootDependencies = graph.root.dependencies.map(o => o.name) + const allPackages = getAllPackages(withRequireChain(graph)) + allPackages.forEach(p => { + rootDependencies.forEach(d => { + const resolveChain = [...p.chain, d] + const key = p.initialDir + ' => ' + resolveChain.join(' => ') + if (alreadyAsserted.has(key)) { + return + } + alreadyAsserted.add(key) + t.ok(setupRequire(path.join(dir, p.initialDir))(...resolveChain), + `Rule 3: ${p.chain.length === 0 && p.initialDir === '.' ? 'The root' : `Package "${[p.initialDir.replace('packages/', ''), ...p.chain].join(' => ')}"`} should have access to "${d}" because it is a root dependency.`) + }) + }) + }, +} + +const rule4 = { + description: 'Packages cannot require packages that are not in their dependencies, not root dependencies or not themselves.', + apply: (t, dir, resolvedGraph, alreadyAsserted) => { + const graph = parseGraph(resolvedGraph) + const allPackages = getAllPackages(withRequireChain(graph)) + const allPackageNames = allPackages.filter(p => p.chain.length !== 0 || p.initialDir !== '.').map(o => o.name) + const rootDependencyNames = graph.root.dependencies.map(o => o.name) + allPackages.forEach(p => { + const resolvedDependencyNames = (p.dependencies || []) + .filter(d => !isLoopToken(d)) + .map(d => d.name) + .concat((p.dependencies || []) + .filter(d => isLoopToken(d)) + .map(t => { + const back = parseLoopToken(t) + return p.chain.slice(-1 - back)[0] // getting the name of the circular dep by going back in the chain + })) + allPackageNames.filter(n => !rootDependencyNames.includes(n)) + .filter(n => !resolvedDependencyNames.includes(n)) + .filter(n => n !== p.name) + .forEach(n => { + const resolveChain = [...p.chain, n] + const key = p.initialDir + ' => ' + resolveChain.join(' => ') + if (alreadyAsserted.has(key)) { + return + } + alreadyAsserted.add(key) + t.notOk(setupRequire(path.join(dir, p.initialDir))(...resolveChain), + `Rule 4: ${p.chain.length === 0 && p.initialDir === '.' ? 'The root' : `Package "${[p.initialDir.replace('packages/', ''), ...p.chain].join(' => ')}"`} should not have access to "${n}" because it not a root dependency, not in its resolved dependencies and not itself.`) + }) + }) + }, +} + +const rule5 = { + description: 'Peer dependencies should be resolved to same instance as parents', + apply: (t, dir, resolvedGraph) => { + const graph = parseGraph(resolvedGraph) + const allPackages = getAllPackages(withRequireChain(graph)) + allPackages.filter(p => p.peer) + .forEach(p => { + const chain = p.chain + const parentChain = chain.slice(0, -2).concat([p.name]) + t.same(setupRequire(path.join(dir, p.initialDir))(...parentChain), setupRequire(path.join(dir, p.initialDir))(...chain), + `Rule 5: Package "${[p.initialDir.replace('packages/', ''), ...chain.slice(0, -1)].join(' => ')}" should get the same instance of "${p.name}" as its parent`) + }) + }, +} + +const rule6 = { + description: 'Packages with the same name, same version, and same peer deps are installed at the same place on disk', + apply: (t, dir, resolvedGraph) => { + const graph = parseGraph(resolvedGraph) + const allPackages = getAllPackages(withRequireChain(graph)) + const byNameAndVersion = new Map() + allPackages.forEach(p => { + const peerDeps = p.dependencies.filter(d => d.peer).map(d => `${d.name}@${d.version}`).sort().join(' - ') + const key = `${p.name}@${p.version} - ${peerDeps}` + if (!byNameAndVersion.has(key)) { + byNameAndVersion.set(key, []) + } + byNameAndVersion.get(key).push(setupRequire(path.join(dir, p.initialDir))(...p.chain)) + }) + byNameAndVersion.forEach((value, key) => { + if (value.length === 1) { + return + } + const same = value.every(l => l === value[0]) + t.ok(same, `Rule 6: Even though it is referenced multiple times, package "${key}" should be installed only once`) + }) + }, +} + +const rule7 = { + description: 'The version of the resolved dependencies is the one we expect', + apply: (t, dir, resolvedGraph) => { + const graph = parseGraph(resolvedGraph) + const allPackages = getAllPackages(withRequireChain(graph)) + allPackages.forEach(p => { + const ppath = setupRequire(path.join(dir, p.initialDir))(...p.chain) + p.dependencies.filter(d => !isLoopToken(d)).forEach(d => { + const dname = d.name + const dversion = JSON.parse(fs.readFileSync(`${resolvePackage(dname, ppath)}/package.json`).toString()).version + + t.ok(dversion === d.version, `Rule 7: The version of ${dname} (${dversion}) provided to ${p.chain.length === 0 && p.initialDir === '.' ? 'the root' : `package "${[p.initialDir.replace('packages/', ''), ...p.chain].join(' => ')}"`} should be "${d.version}"`) + }) + p.dependencies.filter(d => isLoopToken(d)).forEach(token => { + const back = parseLoopToken(token) + const name = p.chain.slice(-1 - back)[0] // getting the name of the circular dep by going back in the chain + const loopStartChain = p.chain.slice(0, -back) + const loopEndChain = [...p.chain, name] + t.same(setupRequire(path.join(dir, p.initialDir))(...loopStartChain), + setupRequire(path.join(dir, p.initialDir))(...loopEndChain), + `The two ends of this dependency loop should resolve to the same location: "${[p.initialDir.replace('packages/', ''), ...loopEndChain].join(' => ')}"`) + }) + }) + }, +} + +tap.test('most simple happy scenario', async t => { + /* + * + * Dependency graph: + * + * foo -> which -> isexe + * + */ + + // Input of arborist + const graph = { + registry: [ + { name: 'which', version: '1.0.0', dependencies: { isexe: '^1.0.0' } }, + { name: 'isexe', version: '1.0.0' }, + ], + root: { + name: 'foo', version: '1.2.3', dependencies: { which: '1.0.0' }, + }, + } + + // expected output + const resolved = { + 'foo@1.2.3 (root)': { + 'which@1.0.0': { + 'isexe@1.0.0': {}, + }, + }, + } + + const { dir, registry } = await getRepo(graph) + + // Note that we override this cache to prevent interference from other tests + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + const asserted = new Set() + rule1.apply(t, dir, resolved, asserted) + rule2.apply(t, dir, resolved, asserted) + rule3.apply(t, dir, resolved, asserted) + rule4.apply(t, dir, resolved, asserted) + rule5.apply(t, dir, resolved, asserted) + rule6.apply(t, dir, resolved, asserted) + rule7.apply(t, dir, resolved, asserted) +}) + +tap.test('simple peer dependencies scenarios', async t => { + /* + * Dependencies: + * + * foo -> tsutils + * tsutils -> typescript (peer dep) + * typescript -> baz + * foo -> typescript + * typescript -> baz + * + */ + + const graph = { + registry: [ + { name: 'tsutils', version: '1.0.0', dependencies: {}, peerDependencies: { typescript: '*' } }, + { name: 'typescript', version: '1.0.0', dependencies: { baz: '*' } }, + { name: 'baz', version: '2.0.0' }, + ], + root: { + name: 'foo', version: '1.2.3', dependencies: { tsutils: '1.0.0', typescript: '1.0.0' }, + }, + } + + const resolved = { + 'foo@1.2.3 (root)': { + 'tsutils@1.0.0': { + 'typescript@1.0.0 (peer)': { + 'baz@2.0.0': {}, + }, + }, + 'typescript@1.0.0': { + 'baz@2.0.0': {}, + }, + }, + } + + const { dir, registry } = await getRepo(graph) + + // Note that we override this cache to prevent interference from other tests + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + const asserted = new Set() + rule1.apply(t, dir, resolved, asserted) + rule2.apply(t, dir, resolved, asserted) + rule3.apply(t, dir, resolved, asserted) + rule4.apply(t, dir, resolved, asserted) + rule5.apply(t, dir, resolved, asserted) + rule6.apply(t, dir, resolved, asserted) + rule7.apply(t, dir, resolved, asserted) +}) + +tap.test('Lock file is same in hoisted and in isolated mode', async t => { + const graph = { + registry: [ + { name: 'which', version: '2.0.2' }, + ], + root: { name: 'foo', version: '1.2.3', dependencies: { which: '2.0.2' } }, + } + + const { dir: hoistedModeDir, registry } = await getRepo(graph) + const { dir: isolatedModeDir } = await getRepo(graph) + + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + const arboristHoisted = new Arborist({ path: hoistedModeDir, registry, packumentCache: new Map(), cache }) + const arboristIsolated = new Arborist({ path: isolatedModeDir, registry, packumentCache: new Map(), cache }) + + await Promise.all([ + arboristHoisted.reify({ strategy: 'hoisted' }), + arboristIsolated.reify({ installStrategy: 'linked' }), + ]) + + const [hoistedModeLockFile, isolatedModeLockFile] = await Promise.all([ + fs.promises.readFile(path.join(hoistedModeDir, 'package-lock.json'), { encoding: 'utf8' }), + fs.promises.readFile(path.join(isolatedModeDir, 'package-lock.json'), { encoding: 'utf8' }), + ]) + + t.same(hoistedModeLockFile, isolatedModeLockFile, 'hoited mode and isolated mode produce the same lockfile') +}) + +tap.test('Basic workspaces setup', async t => { + const graph = { + registry: [ + { name: 'which', version: '1.0.0', dependencies: { isexe: '^1.0.0' } }, + { name: 'which', version: '2.0.0', dependencies: { isexe: '^1.0.0' } }, + { name: 'isexe', version: '1.0.0' }, + ], + root: { + name: 'dog', version: '1.2.3', dependencies: { bar: '*' }, + }, + workspaces: [ + { name: 'bar', version: '1.0.0', dependencies: { which: '2.0.0' } }, + { name: 'baz', version: '1.0.0', dependencies: { which: '2.0.0', bar: '*' } }, + { name: 'cat', version: '1.0.0', dependencies: { which: '1.0.0' } }, + { name: 'fish', version: '1.0.0', dependencies: { which: '1.0.0', cat: '*' } }, + { name: 'catfish', version: '1.0.0' }, + ], + } + + const resolved = { + 'dog@1.2.3 (root)': { + 'bar@1.0.0 (workspace)': { + 'which@2.0.0': { + 'isexe@1.0.0': {}, + }, + }, + 'baz@1.0.0 (workspace)': { + 'bar@1.0.0 (workspace)': { + 'which@2.0.0': { + 'isexe@1.0.0': {}, + }, + }, + 'which@2.0.0': { + 'isexe@1.0.0': {}, + }, + }, + 'cat@1.0.0 (workspace)': { + 'which@1.0.0': { + 'isexe@1.0.0': {}, + }, + }, + 'fish@1.0.0 (workspace)': { + 'cat@1.0.0 (workspace)': { + 'which@1.0.0': { + 'isexe@1.0.0': {}, + }, + }, + 'which@1.0.0': { + 'isexe@1.0.0': {}, + }, + }, + 'catfish@1.0.0': {}, + }, + } + + const { dir, registry } = await getRepo(graph) + + // Note that we override this cache to prevent interference from other tests + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + const asserted = new Set() + rule1.apply(t, dir, resolved, asserted) + rule2.apply(t, dir, resolved, asserted) + rule3.apply(t, dir, resolved, asserted) + rule4.apply(t, dir, resolved, asserted) + rule5.apply(t, dir, resolved, asserted) + rule6.apply(t, dir, resolved, asserted) + rule7.apply(t, dir, resolved, asserted) +}) + +tap.test('resolved versions are the same on isolated and in hoisted mode', async t => { + const graph = { + registry: [ + { name: 'which', version: '1.0.0', dependencies: { isexe: '^1.0.0' } }, + { name: 'which', version: '2.0.0', dependencies: { isexe: '^1.0.0' } }, + { name: 'isexe', version: '1.0.0' }, + ], + root: { + name: 'dog', version: '1.2.3', dependencies: { bar: '*' }, + }, + workspaces: [ + { name: 'bar', version: '1.0.0', dependencies: { which: '2.0.0' } }, + { name: 'baz', version: '1.0.0', dependencies: { which: '2.0.0', bar: '*' } }, + { name: 'cat', version: '1.0.0', dependencies: { which: '1.0.0' } }, + { name: 'fish', version: '1.0.0', dependencies: { which: '1.0.0', cat: '*' } }, + { name: 'catfish', version: '1.0.0' }, + ], + } + + const resolved = { + 'dog@1.2.3 (root)': { + 'bar@1.0.0 (workspace)': { + 'which@2.0.0': { + 'isexe@1.0.0': {}, + }, + }, + }, + 'bar@1.0.0 (workspace)': { + 'which@2.0.0': { + 'isexe@1.0.0': {}, + }, + }, + 'baz@1.0.0 (workspace)': { + 'bar@1.0.0 (workspace)': { + 'which@2.0.0': { + 'isexe@1.0.0': {}, + }, + }, + 'which@2.0.0': { + 'isexe@1.0.0': {}, + }, + }, + 'cat@1.0.0 (workspace)': { + 'which@1.0.0': { + 'isexe@1.0.0': {}, + }, + }, + 'fish@1.0.0 (workspace)': { + 'cat@1.0.0 (workspace)': { + 'which@1.0.0': { + 'isexe@1.0.0': {}, + }, + }, + 'which@1.0.0': { + 'isexe@1.0.0': {}, + }, + }, + 'catfish@1.0.0': {}, + } + + let { dir, registry } = await getRepo(graph) + + // Note that we override this cache to prevent interference from other tests + let cache = fs.mkdtempSync(`${getTempDir()}/test-`) + let arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + rule7.apply(t, dir, resolved, new Set()) + + const mock = await getRepo(graph) + dir = mock.dir + registry = mock.registry + + // Note that we override this cache to prevent interference from other tests + cache = fs.mkdtempSync(`${getTempDir()}/test-`) + arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ strategy: 'hoisted' }) + + // checking that the resolved graph is the same in hoisting and in isolated mode + rule7.apply(t, dir, resolved, new Set()) +}) + +tap.test('peer dependency chain', async t => { + // Input of arborist + const graph = { + registry: [ + { name: 'bar', version: '1.0.0', dependencies: { baz: '^1.0.0' }, peerDependencies: { boat: '*' } }, + { name: 'baz', version: '1.0.0', peerDependencies: { boat: '*' } }, + { name: 'boat', version: '3.0.0' }, + ], + root: { + name: 'foo', version: '1.2.3', dependencies: { bar: '1.0.0', boat: '^3.0.0' }, + }, + } + + // expected output + const resolved = { + 'foo@1.2.3 (root)': { + 'bar@1.0.0': { + 'baz@1.0.0': { + 'boat@3.0.0 (peer)': {}, + }, + 'boat@3.0.0 (peer)': {}, + }, + 'boat@3.0.0': {}, + }, + } + + const { dir, registry } = await getRepo(graph) + + // Note that we override this cache to prevent interference from other tests + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + const asserted = new Set() + rule1.apply(t, dir, resolved, asserted) + rule2.apply(t, dir, resolved, asserted) + rule3.apply(t, dir, resolved, asserted) + rule4.apply(t, dir, resolved, asserted) + rule5.apply(t, dir, resolved, asserted) + rule6.apply(t, dir, resolved, asserted) + rule7.apply(t, dir, resolved, asserted) +}) + +tap.test('failing optional deps are not installed', async t => { + // Input of arborist + const graph = { + registry: [ + { name: 'which', version: '1.0.0', os: ['npmOS'] }, + ], + root: { + name: 'foo', version: '1.2.3', optionalDependencies: { which: '1.0.0' }, + }, + } + + const { dir, registry } = await getRepo(graph) + + // Note that we override this cache to prevent interference from other tests + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + t.notOk(setupRequire(dir)('which'), 'Failing optional deps should not be installed') + + t.notOk(fs.existsSync(path.join(dir, 'node_modules', '.bin', 'which'))) +}) + +tap.test('Optional deps are installed when possible', async t => { + // Input of arborist + const graph = { + registry: [ + { name: 'which', version: '1.0.0' }, + ], + root: { + name: 'foo', version: '1.2.3', optionalDependencies: { which: '1.0.0' }, + }, + } + + const { dir, registry } = await getRepo(graph) + + // Note that we override this cache to prevent interference from other tests + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + t.ok(setupRequire(dir)('which'), 'Optional deps should be installed when possible') + + // TODO: make sure that existsSync is not deprecated + t.ok(fs.existsSync(path.join(dir, 'node_modules', '.bin', 'which'))) +}) + +tap.test('shrinkwrap', async t => { + const shrinkwrap = JSON.stringify({ + name: 'which', + version: '1.0.0', + lockfileVersion: 2, + requires: true, + packages: { + '': { + name: 'which', + version: '1.0.0', + dependencies: { + isexe: '^1.0.0', + }, + }, + 'node_modules/isexe': { + version: '1.0.0', + resolved: '##REG##/isexe/1.0.0.tar', + }, + }, + dependencies: { + isexe: { + version: '1.0.0', + resolved: '##REG##/isexe/1.0.0.tar', + }, + }, + }) + + // Input of arborist + const graph = { + registry: [ + { name: 'which', version: '1.0.0', dependencies: { isexe: '^1.0.0' }, shrinkwrap, _hasShrinkwrap: true }, + { name: 'isexe', version: '1.1.0' }, + { name: 'isexe', version: '1.0.0' }, + ], + root: { + name: 'foo', version: '1.2.3', dependencies: { which: '1.0.0', isexe: '^1.0.0' }, + }, + } + + // expected output + const resolved = { + 'foo@1.2.3 (root)': { + 'which@1.0.0': { + 'isexe@1.0.0': {}, + }, + 'isexe@1.1.0': {}, + }, + } + + const { dir, registry } = await getRepo(graph) + + // Note that we override this cache to prevent interference from other tests + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + const asserted = new Set() + rule1.apply(t, dir, resolved, asserted) + rule2.apply(t, dir, resolved, asserted) + rule3.apply(t, dir, resolved, asserted) + rule4.apply(t, dir, resolved, asserted) + rule5.apply(t, dir, resolved, asserted) + rule6.apply(t, dir, resolved, asserted) + rule7.apply(t, dir, resolved, asserted) +}) + +tap.test('shrinkwrap install dev deps (like hoisting does)', async t => { + const shrinkwrap = JSON.stringify({ + name: 'which', + version: '1.0.0', + lockfileVersion: 2, + requires: true, + packages: { + '': { + name: 'which', + version: '1.0.0', + devDependencies: { + isexe: '^1.0.0', + }, + }, + 'node_modules/isexe': { + version: '1.0.0', + dev: true, + resolved: '##REG##/isexe/1.0.0.tar', + }, + }, + dependencies: { + isexe: { + version: '1.0.0', + dev: true, + resolved: '##REG##/isexe/1.0.0.tar', + }, + }, + }) + + // Input of arborist + const graph = { + registry: [ + { name: 'which', version: '1.0.0', devDependencies: { isexe: '^1.0.0' }, shrinkwrap, _hasShrinkwrap: true }, + { name: 'isexe', version: '1.1.0' }, + { name: 'isexe', version: '1.0.0' }, + ], + root: { + name: 'foo', version: '1.2.3', dependencies: { which: '1.0.0', isexe: '^1.0.0' }, + }, + } + + // expected output + const resolved = { + 'foo@1.2.3 (root)': { + 'which@1.0.0': { + 'isexe@1.0.0': {}, + }, + 'isexe@1.1.0': {}, + }, + } + + const { dir, registry } = await getRepo(graph) + + // Note that we override this cache to prevent interference from other tests + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + const asserted = new Set() + rule1.apply(t, dir, resolved, asserted) + rule2.apply(t, dir, resolved, asserted) + rule3.apply(t, dir, resolved, asserted) + rule4.apply(t, dir, resolved, asserted) + rule5.apply(t, dir, resolved, asserted) + rule6.apply(t, dir, resolved, asserted) + rule7.apply(t, dir, resolved, asserted) +}) + +tap.test('shrinkwrap with peer dependencies', async t => { + const shrinkwrap = JSON.stringify({ + name: 'which', + version: '1.0.0', + lockfileVersion: 2, + requires: true, + packages: { + '': { + name: 'which', + version: '1.0.0', + dependencies: { + isexe: '^1.0.0', + }, + devDependencies: { + bar: '1.0.0', + }, + peerDependencies: { + bar: '*', + }, + }, + 'node_modules/bar': { + version: '1.0.0', + resolved: '##REG##/bar/1.0.0.tar', + dev: true, + bin: { + bar: 'bin.js', + }, + }, + 'node_modules/isexe': { + version: '1.0.0', + resolved: '##REG##/isexe/1.0.0.tar', + bin: { + isexe: 'bin.js', + }, + }, + }, + dependencies: { + bar: { + version: '1.0.0', + resolved: '##REG##/bar/1.0.0.tar', + dev: true, + }, + isexe: { + version: '1.0.0', + resolved: '##REG##/isexe/1.0.0.tar', + }, + }, + }) + // expected output + const resolved = { + 'foo@1.0.0 (root)': { + 'which@1.0.0': { + 'isexe@1.0.0': {}, + 'bar@1.0.0': {}, + }, + 'isexe@1.1.0': {}, + 'bar@1.1.0': {}, + }, + } + + // Input of arborist + const graph = { + registry: [ + { + name: 'which', + version: '1.0.0', + dependencies: { isexe: '^1.0.0' }, + peerDependencies: { bar: '*' }, + devDependencies: { bar: '1.0.0' }, + shrinkwrap, + _hasShrinkwrap: true, + }, + { name: 'isexe', version: '1.1.0' }, + { name: 'isexe', version: '1.0.0' }, + { name: 'bar', version: '1.1.0' }, + { name: 'bar', version: '1.0.0' }, + ], + root: { + name: 'foo', + version: '1.0.0', + dependencies: { isexe: '^1.0.0', bar: '^1.0.0', which: '1.0.0' }, + }, + } + + const { dir, registry } = await getRepo(graph) + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + // TODO: greate the resolved object + const asserted = new Set() + rule1.apply(t, dir, resolved, asserted) + rule2.apply(t, dir, resolved, asserted) + rule3.apply(t, dir, resolved, asserted) + rule4.apply(t, dir, resolved, asserted) + rule5.apply(t, dir, resolved, asserted) + rule6.apply(t, dir, resolved, asserted) + rule7.apply(t, dir, resolved, asserted) +}) + +tap.test('bundled dependencies of external packages', async t => { + // Input of arborist + const graph = { + registry: [ + { name: 'which', + version: '1.0.0', + dependencies: { isexe: '^1.0.0' }, + bundleDependencies: ['isexe'], + bundledDeps: [{ name: 'isexe', version: '1.0.0' }], + }, + { name: 'isexe', version: '1.1.0' }, + ], + root: { + name: 'foo', version: '1.2.3', dependencies: { which: '1.0.0' }, + }, + } + + // expected output + const resolved = { + 'foo@1.2.3 (root)': { + 'which@1.0.0': { + 'isexe@1.0.0': {}, + }, + }, + } + + const { dir, registry } = await getRepo(graph) + + // Note that we override this cache to prevent interference from other tests + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + const asserted = new Set() + rule1.apply(t, dir, resolved, asserted) + rule2.apply(t, dir, resolved, asserted) + rule3.apply(t, dir, resolved, asserted) + rule4.apply(t, dir, resolved, asserted) + rule5.apply(t, dir, resolved, asserted) + rule6.apply(t, dir, resolved, asserted) + rule7.apply(t, dir, resolved, asserted) +}) + +tap.test('bundled dependencies of internal packages', async t => { + // Input of arborist + const graph = { + registry: [ + { name: 'which', version: '1.0.0', dependencies: { isexe: '^1.0.0' }, + }, + { name: 'isexe', version: '1.1.0' }, + ], + root: { + name: 'foo', + version: '1.2.3', + dependencies: { which: '1.0.0', isexe: '^1.0.0' }, + bundleDependencies: ['isexe'], + }, + } + + // expected output + const resolved = { + 'foo@1.2.3 (root)': { + 'which@1.0.0': { + 'isexe@1.1.0': {}, + }, + 'isexe@1.1.0': {}, + }, + } + + const { dir, registry } = await getRepo(graph) + + // Note that we override this cache to prevent interference from other tests + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + const asserted = new Set() + rule1.apply(t, dir, resolved, asserted) + rule2.apply(t, dir, resolved, asserted) + rule3.apply(t, dir, resolved, asserted) + rule4.apply(t, dir, resolved, asserted) + rule5.apply(t, dir, resolved, asserted) + // I think that duplicated versions are okay in the case of bundled deps + // rule6.apply(t, dir, resolved, asserted) + rule7.apply(t, dir, resolved, asserted) + + const isexePath = path.join(dir, 'node_modules', 'isexe') + t.equals(isexePath, fs.realpathSync(isexePath)) +}) + +tap.test('nested bundled dependencies of internal packages', async t => { + // Input of arborist + const graph = { + registry: [ + { name: 'which', version: '1.0.0', dependencies: { isexe: '^1.0.0' }, + }, + { name: 'isexe', version: '1.1.0', dependencies: { bar: '*' } }, + { name: 'bar', version: '3.0.0' }, + ], + root: { + name: 'foo', + version: '1.2.3', + dependencies: { which: '1.0.0', isexe: '^1.0.0' }, + bundleDependencies: ['isexe'], + }, + } + + // expected output + const resolved = { + 'foo@1.2.3 (root)': { + 'which@1.0.0': { + 'isexe@1.1.0': { + 'bar@3.0.0': {}, + }, + }, + 'isexe@1.1.0': {}, + 'bar@3.0.0': {}, // The bundled bar is hoisted + }, + } + + const { dir, registry } = await getRepo(graph) + + // Note that we override this cache to prevent interference from other tests + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + + const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + const asserted = new Set() + rule1.apply(t, dir, resolved, asserted) + rule2.apply(t, dir, resolved, asserted) + rule3.apply(t, dir, resolved, asserted) + rule4.apply(t, dir, resolved, asserted) + rule5.apply(t, dir, resolved, asserted) + // I think that duplicated versions are okay in the case of bundled deps + // rule6.apply(t, dir, resolved, asserted) + rule7.apply(t, dir, resolved, asserted) + + const isexePath = path.join(dir, 'node_modules', 'isexe') + t.equals(isexePath, fs.realpathSync(isexePath)) +}) + +tap.test('nested bundled dependencies of workspaces', async t => { + const graph = { + registry: [ + { name: 'which', version: '2.0.0', dependencies: { isexe: '^1.0.0' } }, + { name: 'isexe', version: '1.0.0' }, + ], + root: { + name: 'dog', version: '1.2.3', + }, + workspaces: [ + { name: 'bar', version: '1.0.0', dependencies: { which: '2.0.0' }, bundleDependencies: ['which'] }, + ], + } + + const resolved = { + 'dog@1.2.3 (root)': { + 'bar@1.0.0 (workspace)': {}, + 'which@2.0.0': {}, + 'isexe@1.0.0': {}, + }, + } + + const { dir, registry } = await getRepo(graph) + + // Note that we override this cache to prevent interference from other tests + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + + const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + const asserted = new Set() + rule1.apply(t, dir, resolved, asserted) + rule2.apply(t, dir, resolved, asserted) + rule3.apply(t, dir, resolved, asserted) + rule4.apply(t, dir, resolved, asserted) + rule5.apply(t, dir, resolved, asserted) + // I think that duplicated versions are okay in the case of bundled deps + // rule6.apply(t, dir, resolved, asserted) + rule7.apply(t, dir, resolved, asserted) + + const isexePath = path.join(dir, 'node_modules', 'isexe') + t.equals(isexePath, fs.realpathSync(isexePath)) + const whichPath = path.join(dir, 'node_modules', 'which') + t.equals(whichPath, fs.realpathSync(whichPath)) +}) + +tap.test('nested bundled dependencies of workspaces with conflicting isolated dep', async t => { + const graph = { + registry: [ + { name: 'which', version: '3.0.0', dependencies: { isexe: '^2.0.0' } }, + { name: 'isexe', version: '2.0.0' }, + { name: 'which', version: '2.0.0', dependencies: { isexe: '^1.0.0' } }, + { name: 'isexe', version: '1.0.0' }, + ], + root: { + name: 'dog', version: '1.2.3', dependencies: { which: '3.0.0' }, + }, + workspaces: [ + { name: 'bar', version: '1.0.0', dependencies: { which: '2.0.0' }, bundleDependencies: ['which'] }, + ], + } + + // the isexe that is bundled is hoisted + // the 'which' that is bundled is not hoisted due to a conflaict + const resolved = { + 'dog@1.2.3 (root)': { + 'bar@1.0.0 (workspace)': { + 'which@2.0.0': { + 'isexe@1.0.0': {}, + }, + 'isexe@1.0.0': {}, + }, + 'which@3.0.0': { + 'isexe@2.0.0': {}, + }, + }, + } + + const { dir, registry } = await getRepo(graph) + + // Note that we override this cache to prevent interference from other tests + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + + const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + const asserted = new Set() + rule1.apply(t, dir, resolved, asserted) + rule2.apply(t, dir, resolved, asserted) + rule3.apply(t, dir, resolved, asserted) + rule4.apply(t, dir, resolved, asserted) + rule5.apply(t, dir, resolved, asserted) + // I think that duplicated versions are okay in the case of bundled deps + // rule6.apply(t, dir, resolved, asserted) + rule7.apply(t, dir, resolved, asserted) + + const isexePath = path.join(dir, 'packages', 'bar', 'node_modules', 'isexe') + t.equals(isexePath, fs.realpathSync(isexePath)) + const whichPath = path.join(dir, 'packages', 'bar', 'node_modules', 'which') + t.equals(whichPath, fs.realpathSync(whichPath)) +}) + +tap.test('adding a dependency', async t => { + // Input of arborist + const graph = { + registry: [ + { name: 'which', version: '1.0.0', dependencies: { isexe: '^1.0.0' } }, + { name: 'isexe', version: '1.0.0' }, + { name: 'bar', version: '2.2.0' }, + ], + root: { + name: 'foo', version: '1.2.3', dependencies: { which: '1.0.0' }, + }, + } + + // expected output + const resolved = { + 'foo@1.2.3 (root)': { + 'which@1.0.0': { + 'isexe@1.0.0': {}, + }, + 'bar@2.2.0': {}, + }, + } + + const { dir, registry } = await getRepo(graph) + + // Note that we override this cache to prevent interference from other tests + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + // Add a new dependency + const cache2 = fs.mkdtempSync(`${getTempDir()}/test-`) + const arborist2 = new Arborist({ path: dir, registry, packumentCache: new Map(), cache: cache2, add: ['bar@^2.0.0'] }) + await arborist2.reify({ installStrategy: 'linked' }) + + // Note that the 'resolved' dependency graph contains 'bar' + const asserted = new Set() + rule1.apply(t, dir, resolved, asserted) + rule2.apply(t, dir, resolved, asserted) + rule3.apply(t, dir, resolved, asserted) + rule4.apply(t, dir, resolved, asserted) + rule5.apply(t, dir, resolved, asserted) + rule6.apply(t, dir, resolved, asserted) + rule7.apply(t, dir, resolved, asserted) +}) + +tap.test('removing a dependency', async t => { + // Input of arborist + const graph = { + registry: [ + { name: 'which', version: '1.0.0', dependencies: { isexe: '^1.0.0' } }, + { name: 'isexe', version: '1.0.0' }, + { name: 'bar', version: '2.2.0' }, + ], + root: { + name: 'foo', version: '1.2.3', dependencies: { which: '1.0.0', bar: '^2.0.0' }, + }, + } + + const { dir, registry } = await getRepo(graph) + + // Note that we override this cache to prevent interference from other tests + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + // checking that bar is installed + t.ok(setupRequire(dir)('bar'), 'bar should be installed initially') + + // Add a new dependency + const cache2 = fs.mkdtempSync(`${getTempDir()}/test-`) + const arborist2 = new Arborist({ path: dir, registry, packumentCache: new Map(), cache: cache2 }) + await arborist2.buildIdealTree({ rm: ['bar'] }) + await arborist2.reify({ installStrategy: 'linked' }) + + t.notOk(setupRequire(dir)('bar'), 'bar should not be installed anymore') +}) + +tap.test('circular dependencies', async t => { + // Input of arborist + const graph = { + registry: [ + { name: 'which', version: '1.0.0', dependencies: { isexe: '^1.0.0' } }, + { name: 'isexe', version: '1.0.0', dependencies: { which: '1.0.0' } }, + { name: 'bar', version: '1.2.6' }, + ], + root: { + name: 'foo', version: '1.2.3', dependencies: { which: '1.0.0', bar: '1.2.6' }, + }, + } + + // expected output + const resolved = { + 'foo@1.2.3 (root)': { + 'which@1.0.0': { + 'isexe@1.0.0': '(back 1)', + }, + 'bar@1.2.6': {}, + }, + } + + const { dir, registry } = await getRepo(graph) + + // Note that we override this cache to prevent interference from other tests + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + const asserted = new Set() + rule1.apply(t, dir, resolved, asserted) + rule2.apply(t, dir, resolved, asserted) + rule3.apply(t, dir, resolved, asserted) + rule4.apply(t, dir, resolved, asserted) + rule5.apply(t, dir, resolved, asserted) + rule6.apply(t, dir, resolved, asserted) + rule7.apply(t, dir, resolved, asserted) +}) + +tap.test('circular peer dependencies', async t => { + // Input of arborist + const graph = { + registry: [ + { name: 'cat', version: '1.0.0', peerDependencies: { bar: '*' } }, + { name: 'bar', version: '1.0.0', peerDependencies: { cat: '*' } }, + ], + root: { + name: 'foo', version: '1.2.3', dependencies: { cat: '1.0.0', bar: '1.0.0' }, + }, + } + + // expected output + const resolved = { + 'foo@1.2.3 (root)': { + 'cat@1.0.0': { + 'bar@1.0.0': '(back 1)', + }, + 'bar@1.0.0': { + 'cat@1.0.0': '(back 1)', + }, + }, + } + + const { dir, registry } = await getRepo(graph) + + // Note that we override this cache to prevent interference from other tests + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + const asserted = new Set() + rule1.apply(t, dir, resolved, asserted) + rule2.apply(t, dir, resolved, asserted) + rule3.apply(t, dir, resolved, asserted) + rule4.apply(t, dir, resolved, asserted) + rule5.apply(t, dir, resolved, asserted) + rule6.apply(t, dir, resolved, asserted) + rule7.apply(t, dir, resolved, asserted) +}) + +tap.test('peer dependency on parent', async t => { + // Input of arborist + const graph = { + registry: [ + { name: 'cat', version: '1.0.0', dependencies: { bar: '*' } }, + { name: 'bar', version: '1.0.0', peerDependencies: { cat: '*' } }, + ], + root: { + name: 'foo', version: '1.2.3', dependencies: { cat: '1.0.0' }, + }, + } + + // expected output + const resolved = { + 'foo@1.2.3 (root)': { + 'cat@1.0.0': { + 'bar@1.0.0': '(back 1)', + }, + }, + } + + const { dir, registry } = await getRepo(graph) + + // Note that we override this cache to prevent interference from other tests + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + const asserted = new Set() + rule1.apply(t, dir, resolved, asserted) + rule2.apply(t, dir, resolved, asserted) + rule3.apply(t, dir, resolved, asserted) + rule4.apply(t, dir, resolved, asserted) + rule5.apply(t, dir, resolved, asserted) + rule6.apply(t, dir, resolved, asserted) + rule7.apply(t, dir, resolved, asserted) +}) + +tap.test('scoped package', async t => { + /* + * + * Dependency graph: + * + * foo -> which -> isexe + * + */ + + // Input of arborist + const graph = { + registry: [ + { name: 'which', version: '1.0.0', dependencies: { '@foo/isexe': '^1.0.0' } }, + { name: '@foo/isexe', version: '1.0.0' }, + ], + root: { + name: 'foo', version: '1.2.3', dependencies: { which: '1.0.0' }, + }, + } + + // expected output + const resolved = { + 'foo@1.2.3 (root)': { + 'which@1.0.0': { + '@foo/isexe@1.0.0': {}, + }, + }, + } + + const { dir, registry } = await getRepo(graph) + + // Note that we override this cache to prevent interference from other tests + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + const asserted = new Set() + rule1.apply(t, dir, resolved, asserted) + rule2.apply(t, dir, resolved, asserted) + rule3.apply(t, dir, resolved, asserted) + rule4.apply(t, dir, resolved, asserted) + rule7.apply(t, dir, resolved, asserted) +}) + +tap.test('failing optional peer deps are not installed', async t => { + // Input of arborist + const graph = { + registry: [ + { name: 'which', version: '1.0.0', os: ['npmOS'] }, + { name: 'bar', version: '1.0.0', peerDependencies: { which: '*' }, peerDependenciesMeta: { which: { optional: true } } }, + ], + root: { + name: 'foo', version: '1.2.3', optionalDependencies: { which: '1.0.0', bar: '*' }, + }, + } + + const { dir, registry } = await getRepo(graph) + + // Note that we override this cache to prevent interference from other tests + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + t.notOk(setupRequire(dir)('bar', 'which'), 'Failing optional peer deps should not be installed') +}) + +// Virtual packages are 2 packages that have the same version but are +// duplicated on disk to solve peer-dependency conflict. +tap.test('virtual packages', async t => { + // Input of arborist + const graph = { + registry: [ + { name: 'foo', version: '1.0.0' }, + { name: 'foo', version: '2.0.0', peerDependencies: { cat: '*' } }, + { name: 'cat', version: '1.0.0' }, + { name: 'cat', version: '2.0.0' }, + { name: 'bar', version: '1.0.0', dependencies: { foo: '2.0.0', cat: '2.0.0' } }, + { name: 'baz', version: '1.0.0', dependencies: { foo: '2.0.0', cat: '1.0.0' } }, + ], + root: { + name: 'toor', version: '1.2.3', dependencies: { foo: '1.0.0', bar: '*', baz: '*', cat: '1.0.0' }, + }, + } + + // expected output + const resolved = { + 'toor@1.2.3 (root)': { + 'foo@1.0.0': {}, + 'bar@1.0.0': { + 'foo@2.0.0': { + 'cat@2.0.0 (peer)': {}, + }, + 'cat@2.0.0': {}, + }, + 'baz@1.0.0': { + 'foo@2.0.0': { + 'cat@1.0.0 (peer)': {}, + }, + 'cat@1.0.0': {}, + }, + 'cat@1.0.0': {}, + }, + } + + const { dir, registry } = await getRepo(graph) + + // Note that we override this cache to prevent interference from other tests + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + const asserted = new Set() + rule1.apply(t, dir, resolved, asserted) + rule2.apply(t, dir, resolved, asserted) + rule3.apply(t, dir, resolved, asserted) + rule4.apply(t, dir, resolved, asserted) + rule5.apply(t, dir, resolved, asserted) + rule6.apply(t, dir, resolved, asserted) + rule7.apply(t, dir, resolved, asserted) +}) + +tap.test('postinstall scripts are run', async t => { + // Input of arborist + const graph = { + registry: [ + { name: 'which', version: '1.0.0', scripts: { postinstall: `node -e "fs.openSync('postInstallRanWhich', 'w')"` } }, + ], + root: { + name: 'foo', version: '1.2.3', dependencies: { which: '1.0.0' }, + }, + workspaces: [ + { name: 'bar', version: '1.0.0', scripts: { postinstall: `node -e "fs.openSync('postInstallRanBar', 'w')"` } }, + ], + } + + const { dir, registry } = await getRepo(graph) + + // Note that we override this cache to prevent interference from other tests + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + const postInstallRanWhich = pathExists(`${setupRequire(dir)('which')}/postInstallRanWhich`) + t.ok(postInstallRanWhich) + + const postInstallRanBar = pathExists(`${setupRequire(dir)('bar')}/postInstallRanBar`) + t.ok(postInstallRanBar) +}) + +tap.test('bins are installed', async t => { + // Input of arborist + const graph = { + registry: [ + { name: 'which', version: '1.0.0', bin: './bin.js' }, + ], + root: { + name: 'foo', version: '1.2.3', dependencies: { which: '1.0.0' }, + }, + workspaces: [ + { name: 'bar', version: '1.0.0', dependencies: { which: '1.0.0' }, bin: './bin.js' }, + ], + } + + const { dir, registry } = await getRepo(graph) + + // Note that we override this cache to prevent interference from other tests + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arborist.reify({ installStrategy: 'linked' }) + + // TODO: make the test not assume folder structure + // TODO should the bin also be in the store? + // const binFromWhichToWhich = pathExists(fs.realpathSync(`${setupRequire(dir)('which')}/../.bin/which`)) + // t.ok(binFromWhichToWhich) + + const binFromRootToWhich = pathExists(`${dir}/node_modules/.bin/which`) + t.ok(binFromRootToWhich) + + const binFromRootToBar = pathExists(`${dir}/node_modules/.bin/bar`) + t.ok(binFromRootToBar) + + const binFromBarToWhich = pathExists(`${setupRequire(dir)('bar')}/node_modules/.bin/which`) + t.ok(binFromBarToWhich) +}) + +function setupRequire (cwd) { + return function requireChain (...chain) { + return chain.reduce((path, name) => { + if (path === undefined) { + return undefined + } + return resolvePackage(name, path) + }, cwd) + } +} + +function pathExists (path) { + try { + fs.statSync(path) + return true + } catch (_) { + return false + } +} + +/** + * We reimplement a lightweight version of require.resolve because the + * one that is implemented in nodejs memoizes the resolution which + * asserts interfering with each others + **/ +function resolvePackage (name, from) { + try { + const loc = `${from}/node_modules/${name}` + fs.statSync(loc) + return fs.realpathSync(loc) + } catch (_) { + const next = path.dirname(from) + if (next === from) { + return undefined + } else { + return resolvePackage(name, next) + } + } +} + +function getAllPackages (resolvedGraph) { + return [...getAllPackagesRecursive(resolvedGraph.root), + ...(resolvedGraph.workspaces?.map(w => getAllPackagesRecursive(w)) || []).reduce((a, n) => ([...a, ...n]), [])] +} + +function getAllPackagesRecursive (resolvedGraph) { + return [ + resolvedGraph, + ...(resolvedGraph.dependencies + ?.filter(d => !isLoopToken(d)) + .map(d => getAllPackagesRecursive(d)) || []) + .reduce((a, n) => ([...a, ...n]), []), + ] +} + +function withRequireChain (resolvedGraph) { + return { + root: { + ...resolvedGraph.root, + chain: [], + initialDir: '.', + dependencies: resolvedGraph.root.dependencies?.map(d => + withRequireChainRecursive(d, [], '.')), + }, + workspaces: resolvedGraph.workspaces?.map(w => { + const initialDir = `packages/${w.name}` + return { + ...w, + chain: [], + initialDir, + dependencies: w.dependencies?.map(d => withRequireChainRecursive(d, [], initialDir)), + } + }), + } +} + +function withRequireChainRecursive (resolvedGraph, chain, initialDir) { + if (isLoopToken(resolvedGraph)) { + return resolvedGraph + } + + const newChain = [...chain, resolvedGraph.name] + return { + ...resolvedGraph, + chain: newChain, + initialDir, + dependencies: resolvedGraph.dependencies?.map(d => + withRequireChainRecursive(d, newChain, initialDir)), + } +} + +function parseGraph (graph) { + const root = Object.entries(graph).find(([key]) => key.includes('(root)')) + const result = { root: parseGraphRecursive(...root), workspaces: [] } + + Object.entries(graph).filter(([key]) => key.includes('(workspace)')) + .forEach(([key, value]) => { + result.workspaces.push(parseGraphRecursive(key, value)) + }) + return result +} + +function isLoopToken (obj) { + return typeof obj === 'string' && /^\(back \d+\)$/.test(obj) +} + +function parseLoopToken (t) { + return parseInt(/\d+/.exec(t)[0]) +} + +function parseGraphRecursive (key, deps) { + if (isLoopToken(key)) { + return key + } + const name = /^(.[^@]*)@/.exec(key)[1] + const version = /^.[^@]*@([^ ]*)/.exec(key)[1] + const workspace = / \(workspace\)/.test(key) + const peer = / \(peer\)/.test(key) + const normalizedDeps = typeof deps === 'string' ? { [deps]: {} } : deps + const dependencies = Object.entries(normalizedDeps).map(([key, value]) => parseGraphRecursive(key, value)) + return { name, version, workspace, peer, dependencies } +} + +/* + * TO TEST: + * -------------------------------------- + * - rollbacks + * - scoped installs + * - overrides? + * - changing repo from isolated to hoisted and from hoisted to isolated + */