├── test ├── fixtures │ ├── deep │ │ └── .keep │ ├── other │ │ └── node_modules │ │ │ └── .bin │ ├── root │ │ ├── node_modules │ │ │ ├── .bin │ │ │ ├── .foo │ │ │ ├── @scope │ │ │ │ ├── x │ │ │ │ │ ├── node_modules │ │ │ │ │ │ ├── .bin │ │ │ │ │ │ └── glob │ │ │ │ │ │ │ ├── node_modules │ │ │ │ │ │ │ ├── .bin │ │ │ │ │ │ │ ├── minimatch │ │ │ │ │ │ │ │ ├── node_modules │ │ │ │ │ │ │ │ │ ├── .bin │ │ │ │ │ │ │ │ │ ├── sigmund │ │ │ │ │ │ │ │ │ │ └── package.json │ │ │ │ │ │ │ │ │ └── lru-cache │ │ │ │ │ │ │ │ │ │ └── package.json │ │ │ │ │ │ │ │ └── package.json │ │ │ │ │ │ │ ├── once │ │ │ │ │ │ │ │ └── package.json │ │ │ │ │ │ │ ├── inherits │ │ │ │ │ │ │ │ └── package.json │ │ │ │ │ │ │ └── graceful-fs │ │ │ │ │ │ │ │ └── package.json │ │ │ │ │ │ │ └── package.json │ │ │ │ │ └── package.json │ │ │ │ └── y │ │ │ │ │ └── package.json │ │ │ └── foo │ │ │ │ ├── package.json │ │ │ │ └── node_modules │ │ │ │ └── express │ │ │ │ └── package.json │ │ └── package.json │ ├── selflink │ │ ├── node_modules │ │ │ ├── .bin │ │ │ ├── foo │ │ │ │ ├── node_modules │ │ │ │ │ ├── .bin │ │ │ │ │ ├── selflink │ │ │ │ │ └── glob │ │ │ │ │ │ ├── node_modules │ │ │ │ │ │ ├── .bin │ │ │ │ │ │ ├── minimatch │ │ │ │ │ │ │ ├── node_modules │ │ │ │ │ │ │ │ ├── .bin │ │ │ │ │ │ │ │ ├── sigmund │ │ │ │ │ │ │ │ │ └── package.json │ │ │ │ │ │ │ │ └── lru-cache │ │ │ │ │ │ │ │ │ └── package.json │ │ │ │ │ │ │ └── package.json │ │ │ │ │ │ ├── once │ │ │ │ │ │ │ └── package.json │ │ │ │ │ │ ├── inherits │ │ │ │ │ │ │ └── package.json │ │ │ │ │ │ └── graceful-fs │ │ │ │ │ │ │ └── package.json │ │ │ │ │ │ └── package.json │ │ │ │ └── package.json │ │ │ └── @scope │ │ │ │ ├── z │ │ │ │ ├── node_modules │ │ │ │ │ └── .bin │ │ │ │ └── package.json │ │ │ │ └── y │ │ │ │ └── package.json │ │ └── package.json │ ├── noname │ │ └── node_modules │ │ │ └── foo │ │ │ └── keep-alive │ ├── bad │ │ └── package.json │ └── empty │ │ └── node_modules │ │ └── foo │ │ └── package.json ├── symlinked-node-modules.js └── basic.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── package.json ├── README.md ├── realpath.js ├── rpt.js └── tap-snapshots └── test-basic.js-TAP.test.js /test/fixtures/deep/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/other/node_modules/.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/root/node_modules/.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/root/node_modules/.foo: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/selflink/node_modules/.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/noname/node_modules/foo/keep-alive: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/bad/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "NOPE" 3 | -------------------------------------------------------------------------------- /test/fixtures/empty/node_modules/foo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | -------------------------------------------------------------------------------- /test/fixtures/root/node_modules/@scope/x/node_modules/.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/selflink/node_modules/foo/node_modules/.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.nyc_output/ 3 | /coverage/ 4 | -------------------------------------------------------------------------------- /test/fixtures/selflink/node_modules/@scope/z/node_modules/.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/root/package.json: -------------------------------------------------------------------------------- 1 | {"name":"root", 2 | "version":"1.2.3"} -------------------------------------------------------------------------------- /test/fixtures/selflink/node_modules/foo/node_modules/selflink: -------------------------------------------------------------------------------- 1 | ../../.. -------------------------------------------------------------------------------- /test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/selflink/node_modules/foo/node_modules/glob/node_modules/.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/selflink/package.json: -------------------------------------------------------------------------------- 1 | {"name":"selflink", 2 | "version":"1.2.3"} 3 | -------------------------------------------------------------------------------- /test/fixtures/root/node_modules/foo/package.json: -------------------------------------------------------------------------------- 1 | {"name":"foo","version":"1.2.3"} 2 | -------------------------------------------------------------------------------- /test/fixtures/selflink/node_modules/foo/package.json: -------------------------------------------------------------------------------- 1 | {"name":"foo","version":"1.2.3"} 2 | -------------------------------------------------------------------------------- /test/fixtures/selflink/node_modules/foo/node_modules/glob/node_modules/minimatch/node_modules/.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '12' 5 | - '10' 6 | - '8' 7 | -------------------------------------------------------------------------------- /test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/minimatch/node_modules/.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/root/node_modules/@scope/x/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"@scope/x", 3 | "version":"1.2.3" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/root/node_modules/@scope/y/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"@scope/y", 3 | "version":"1.2.3" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/selflink/node_modules/@scope/y/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"@scope/y", 3 | "version":"1.2.3" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/selflink/node_modules/@scope/z/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"@scope/z", 3 | "version":"1.2.3" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/root/node_modules/@scope/x/node_modules/glob/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glob", 3 | "version": "4.0.5" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/selflink/node_modules/foo/node_modules/glob/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glob", 3 | "version": "4.0.5" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/once/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "once", 3 | "version": "1.3.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/selflink/node_modules/foo/node_modules/glob/node_modules/once/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "once", 3 | "version": "1.3.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/inherits/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inherits", 3 | "version": "2.0.1" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/minimatch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minimatch", 3 | "version": "1.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/selflink/node_modules/foo/node_modules/glob/node_modules/inherits/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inherits", 3 | "version": "2.0.1" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/selflink/node_modules/foo/node_modules/glob/node_modules/minimatch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minimatch", 3 | "version": "1.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/graceful-fs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graceful-fs", 3 | "version": "3.0.2" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/selflink/node_modules/foo/node_modules/glob/node_modules/graceful-fs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graceful-fs", 3 | "version": "3.0.2" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/minimatch/node_modules/sigmund/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sigmund", 3 | "version": "1.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/selflink/node_modules/foo/node_modules/glob/node_modules/minimatch/node_modules/sigmund/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sigmund", 3 | "version": "1.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/minimatch/node_modules/lru-cache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lru-cache", 3 | "version": "2.5.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/selflink/node_modules/foo/node_modules/glob/node_modules/minimatch/node_modules/lru-cache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lru-cache", 3 | "version": "2.5.0" 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The ISC License 2 | 3 | Copyright (c) Isaac Z. Schlueter and Contributors 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 15 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "read-package-tree", 3 | "version": "5.3.1", 4 | "description": "Read the contents of node_modules.", 5 | "main": "rpt.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "dependencies": { 10 | "read-package-json": "^2.0.0", 11 | "readdir-scoped-modules": "^1.0.0", 12 | "util-promisify": "^2.1.0" 13 | }, 14 | "devDependencies": { 15 | "archy": "^1.0.0", 16 | "mkdirp": "^0.5.1", 17 | "tacks": "^1.2.1", 18 | "tap": "^12.7.0" 19 | }, 20 | "scripts": { 21 | "test": "tap test/*.js --100", 22 | "snap": "TAP_SNAPSHOT=1 tap test/*.js --100", 23 | "preversion": "npm test", 24 | "postversion": "npm publish", 25 | "postpublish": "git push origin --follow-tags" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/npm/read-package-tree" 30 | }, 31 | "author": "Isaac Z. Schlueter (http://blog.izs.me/)", 32 | "license": "ISC", 33 | "bugs": { 34 | "url": "https://github.com/npm/read-package-tree/issues" 35 | }, 36 | "homepage": "https://github.com/npm/read-package-tree", 37 | "files": [ 38 | "rpt.js", 39 | "realpath.js" 40 | ], 41 | "tap": { 42 | "100": true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/symlinked-node-modules.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var path = require('path') 3 | var test = require('tap').test 4 | var rpt = require('../rpt.js') 5 | var Tacks = require('tacks') 6 | var File = Tacks.File 7 | var Symlink = Tacks.Symlink 8 | var Dir = Tacks.Dir 9 | 10 | var workdir = path.join(__dirname, path.basename(__filename, '.js')) 11 | var fixture = new Tacks(Dir({ 12 | bar: Dir({ 13 | 'package.json': File({ 14 | name: 'bar', 15 | version: '1.0.0' 16 | }) 17 | }), 18 | 'linked-node-modules': Dir({ 19 | bar: Symlink('../bar'), 20 | foo: Dir({ 21 | 'package.json': File({ 22 | name: 'foo', 23 | version: '1.0.0' 24 | }) 25 | }) 26 | }), 27 | example: Dir({ 28 | node_modules: Symlink('../linked-node-modules/'), 29 | 'package.json': File({ 30 | name: 'example', 31 | version: '1.0.0', 32 | }) 33 | }) 34 | })) 35 | 36 | function setup () { 37 | cleanup() 38 | fixture.create(workdir) 39 | } 40 | 41 | function cleanup () { 42 | fixture.remove(workdir) 43 | } 44 | 45 | test('setup', function (t) { 46 | setup() 47 | t.done() 48 | }) 49 | test('symlinked-node-modules', function (t) { 50 | rpt(path.join(workdir, 'example'), function (err, tree) { 51 | t.ifError(err) 52 | t.is(tree.children.length, 2) 53 | var childrenShouldBe = { 54 | 'foo': {isLink: false}, 55 | 'bar': {isLink: true} 56 | } 57 | tree.children.forEach(function (child) { 58 | var name = child.package.name 59 | t.is(child.isLink, childrenShouldBe[name].isLink, 60 | 'is' + (childrenShouldBe[name].isLink ? '' : 'Not') + 'Link ' + 61 | path.relative(workdir, child.path) + " + " + 62 | path.relative(workdir, child.realpath)) 63 | }) 64 | t.done() 65 | }) 66 | }) 67 | test('cleanup', function (t) { 68 | cleanup() 69 | t.done() 70 | }) -------------------------------------------------------------------------------- /test/fixtures/root/node_modules/foo/node_modules/express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_from": "express@npm:abbrev@*", 3 | "_id": "abbrev@1.1.1", 4 | "_inBundle": false, 5 | "_integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", 6 | "_location": "/express", 7 | "_phantomChildren": {}, 8 | "_requested": { 9 | "type": "alias", 10 | "registry": true, 11 | "raw": "express@npm:abbrev@*", 12 | "name": "express", 13 | "escapedName": "express", 14 | "rawSpec": "npm:abbrev@*", 15 | "saveSpec": null, 16 | "fetchSpec": null, 17 | "subSpec": { 18 | "type": "range", 19 | "registry": true, 20 | "raw": "abbrev@*", 21 | "name": "abbrev", 22 | "escapedName": "abbrev", 23 | "rawSpec": "*", 24 | "saveSpec": null, 25 | "fetchSpec": "*" 26 | } 27 | }, 28 | "_requiredBy": [ 29 | "#USER", 30 | "/" 31 | ], 32 | "_resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 33 | "_shasum": "f8f2c887ad10bf67f634f005b6987fed3179aac8", 34 | "_spec": "express@npm:abbrev@*", 35 | "_where": "/Users/isaacs/dev/npm/read-package-tree", 36 | "author": { 37 | "name": "Isaac Z. Schlueter", 38 | "email": "i@izs.me" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/isaacs/abbrev-js/issues" 42 | }, 43 | "bundleDependencies": false, 44 | "deprecated": false, 45 | "description": "Like ruby's abbrev module, but in js", 46 | "devDependencies": { 47 | "tap": "^10.1" 48 | }, 49 | "files": [ 50 | "abbrev.js" 51 | ], 52 | "homepage": "https://github.com/isaacs/abbrev-js#readme", 53 | "license": "ISC", 54 | "main": "abbrev.js", 55 | "name": "abbrev", 56 | "repository": { 57 | "type": "git", 58 | "url": "git+ssh://git@github.com/isaacs/abbrev-js.git" 59 | }, 60 | "scripts": { 61 | "postpublish": "git push origin --all; git push origin --tags", 62 | "postversion": "npm publish", 63 | "preversion": "npm test", 64 | "test": "tap test.js --100" 65 | }, 66 | "version": "1.1.1" 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # read-package-tree 2 | 3 | [![Build Status](https://travis-ci.org/npm/read-package-tree.svg?branch=master)](https://travis-ci.org/npm/read-package-tree) 4 | 5 | Read the contents of node_modules. 6 | 7 | ## USAGE 8 | 9 | ```javascript 10 | var rpt = require ('read-package-tree') 11 | rpt('/path/to/pkg/root', function (node, kidName) { 12 | // optional filter function– if included, each package folder found is passed to 13 | // it to see if it should be included in the final tree 14 | // node is what we're adding children to 15 | // kidName is the directory name of the module we're considering adding 16 | // return true -> include, false -> skip 17 | }, function (er, data) { 18 | // er means that something didn't work. 19 | // data is a structure like: 20 | // { 21 | // package: 22 | // package.name: defaults to `basename(path)` 23 | // children: [ ] 24 | // parent: 25 | // path: 26 | // realpath: 27 | // isLink: 28 | // target: 29 | // error: 30 | // } 31 | }) 32 | 33 | // or promise-style 34 | rpt('/path/to/pkg/root').then(data => { ... }) 35 | ``` 36 | 37 | That's it. It doesn't figure out if dependencies are met, it doesn't 38 | mutate package.json data objects (beyond what 39 | [read-package-json](http://npm.im/read-package-json) already does), it 40 | doesn't limit its search to include/exclude `devDependencies`, or 41 | anything else. 42 | 43 | Just follows the links in the `node_modules` hierarchy and reads the 44 | package.json files it finds therein. 45 | 46 | ## Symbolic Links 47 | 48 | When there are symlinks to packages in the `node_modules` hierarchy, a 49 | `Link` object will be created, with a `target` that is a `Node` 50 | object. 51 | 52 | For the most part, you can treat `Link` objects just the same as 53 | `Node` objects. But if your tree-walking program needs to treat 54 | symlinks differently from normal folders, then make sure to check the 55 | object. 56 | 57 | In a given `read-package-tree` run, a specific `path` will always 58 | correspond to a single object, and a specific `realpath` will always 59 | correspond to a single `Node` object. This means that you may not be 60 | able to pass the resulting data object to `JSON.stringify`, because it 61 | may contain cycles. 62 | 63 | ## Errors 64 | 65 | Errors parsing or finding a package.json in node_modules will result in a 66 | node with the error property set. We will still find deeper node_modules 67 | if any exist. *Prior to `5.0.0` these aborted tree reading with an error 68 | callback.* 69 | 70 | Only a few classes of errors are fatal (result in an error callback): 71 | 72 | * If the top level location is entirely missing, that will error. 73 | * if `fs.realpath` returns an error for any path its trying to resolve. 74 | -------------------------------------------------------------------------------- /realpath.js: -------------------------------------------------------------------------------- 1 | // look up the realpath, but cache stats to minimize overhead 2 | // If the parent folder is in the realpath cache, then we just 3 | // lstat the child, since there's no need to do a full realpath 4 | // This is not a separate module, and is much simpler than Node's 5 | // built-in fs.realpath, because we only care about symbolic links, 6 | // so we can handle many fewer edge cases. 7 | 8 | const fs = require('fs') 9 | /* istanbul ignore next */ 10 | const promisify = require('util').promisify || require('util-promisify') 11 | const readlink = promisify(fs.readlink) 12 | const lstat = promisify(fs.lstat) 13 | const { resolve, basename, dirname } = require('path') 14 | 15 | const realpathCached = (path, rpcache, stcache, depth) => { 16 | // just a safety against extremely deep eloops 17 | /* istanbul ignore next */ 18 | if (depth > 2000) 19 | throw eloop(path) 20 | 21 | path = resolve(path) 22 | if (rpcache.has(path)) 23 | return Promise.resolve(rpcache.get(path)) 24 | 25 | const dir = dirname(path) 26 | const base = basename(path) 27 | 28 | if (base && rpcache.has(dir)) 29 | return realpathChild(dir, base, rpcache, stcache, depth) 30 | 31 | // if it's the root, then we know it's real 32 | if (!base) { 33 | rpcache.set(dir, dir) 34 | return Promise.resolve(dir) 35 | } 36 | 37 | // the parent, what is that? 38 | // find out, and then come back. 39 | return realpathCached(dir, rpcache, stcache, depth + 1).then(() => 40 | realpathCached(path, rpcache, stcache, depth + 1)) 41 | } 42 | 43 | const lstatCached = (path, stcache) => { 44 | if (stcache.has(path)) 45 | return Promise.resolve(stcache.get(path)) 46 | 47 | const p = lstat(path).then(st => { 48 | stcache.set(path, st) 49 | return st 50 | }) 51 | stcache.set(path, p) 52 | return p 53 | } 54 | 55 | // This is a slight fib, as it doesn't actually occur during a stat syscall. 56 | // But file systems are giant piles of lies, so whatever. 57 | const eloop = path => 58 | Object.assign(new Error( 59 | `ELOOP: too many symbolic links encountered, stat '${path}'`), { 60 | errno: -62, 61 | syscall: 'stat', 62 | code: 'ELOOP', 63 | path: path, 64 | }) 65 | 66 | const realpathChild = (dir, base, rpcache, stcache, depth) => { 67 | const realdir = rpcache.get(dir) 68 | // that unpossible 69 | /* istanbul ignore next */ 70 | if (typeof realdir === 'undefined') 71 | throw new Error('in realpathChild without parent being in realpath cache') 72 | 73 | const realish = resolve(realdir, base) 74 | return lstatCached(realish, stcache).then(st => { 75 | if (!st.isSymbolicLink()) { 76 | rpcache.set(resolve(dir, base), realish) 77 | return realish 78 | } 79 | 80 | let res 81 | return readlink(realish).then(target => { 82 | const resolved = res = resolve(realdir, target) 83 | if (realish === resolved) 84 | throw eloop(realish) 85 | 86 | return realpathCached(resolved, rpcache, stcache, depth + 1) 87 | }).then(real => { 88 | rpcache.set(resolve(dir, base), real) 89 | return real 90 | }) 91 | }) 92 | } 93 | 94 | module.exports = realpathCached 95 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test 2 | var rpt = require('../rpt.js') 3 | var path = require('path') 4 | var resolve = path.resolve 5 | var fs = require('fs') 6 | var archy = require('archy') 7 | var mkdirp = require('mkdirp') 8 | var fixtures = path.resolve(__dirname, 'fixtures') 9 | var roots = [ 'root', 'other', 'selflink', 'noname' ] 10 | var cwd = path.resolve(__dirname, '..') 11 | 12 | var symlinks = { 13 | 'selflink/node_modules/@scope/z/node_modules/glob': 14 | '../../../foo/node_modules/glob', 15 | 'other/node_modules/glob': 16 | '../../root/node_modules/@scope/x/node_modules/glob', 17 | 'linkedroot': 18 | 'root', 19 | 'deep/root': 20 | '../root', 21 | 'deeproot': 22 | 'deep', 23 | 'badlink/node_modules/foo': 24 | 'foo', 25 | 'badlink/node_modules/bar': 26 | 'baz' 27 | } 28 | 29 | function cleanup () { 30 | Object.keys(symlinks).forEach(function (s) { 31 | var p = path.resolve(cwd, 'test/fixtures', s) 32 | try { 33 | fs.unlinkSync(p) 34 | } catch (er) {} 35 | }) 36 | } 37 | 38 | test('setup symlinks', function (t) { 39 | cleanup() 40 | 41 | Object.keys(symlinks).forEach(function (s) { 42 | var p = path.resolve(cwd, 'test/fixtures', s) 43 | mkdirp.sync(path.dirname(p)) 44 | fs.symlinkSync(symlinks [ s ], p, 'dir') 45 | }) 46 | 47 | t.end() 48 | }) 49 | 50 | roots.forEach(function (root) { 51 | var dir = path.resolve(fixtures, root) 52 | 53 | // test promisey nature 54 | test(root, t => rpt(dir).then(d => 55 | t.matchSnapshot(archy(archyize(d)).trim(), root + ' tree'))) 56 | }) 57 | 58 | test('linkedroot', function (t) { 59 | var dir = path.resolve(fixtures, 'linkedroot') 60 | // test legacy cb nature 61 | rpt(dir, function (er, d) { 62 | if (er && er.code !== 'ENOENT') throw er 63 | 64 | var actual = archy(archyize(d)).trim() 65 | t.matchSnapshot(actual, 'linkedroot tree') 66 | t.end() 67 | }) 68 | }) 69 | 70 | test('deeproot', function (t) { 71 | var dir = path.resolve(fixtures, 'deeproot/root') 72 | rpt(dir, function (er, d) { 73 | if (er && er.code !== 'ENOENT') throw er 74 | 75 | var actual = archy(archyize(d)).trim() 76 | t.matchSnapshot(actual, 'deeproot tree') 77 | t.end() 78 | }) 79 | }) 80 | 81 | test('filterWith', t => 82 | rpt( 83 | path.join(fixtures, 'root'), 84 | (node, kid) => !node.parent, 85 | true 86 | ).then(d => t.matchSnapshot(archy(archyize(d)).trim()), 'only 1 level deep') 87 | ) 88 | 89 | test('looking outside of cwd', t => { 90 | const cwd = process.cwd() 91 | t.teardown(() => process.chdir(cwd)) 92 | process.chdir('test/fixtures/selflink') 93 | return rpt('../root').then(d => 94 | t.matchSnapshot(archy(archyize(d)).trim())) 95 | }) 96 | 97 | test('shake out Link target timing issue', t => { 98 | process.env._TEST_RPT_SLOW_LINK_TARGET_ = '1' 99 | const cwd = process.cwd() 100 | t.teardown(() => process.env._TEST_RPT_SLOW_LINK_TARGET_ = '') 101 | return rpt(path.resolve(fixtures, 'selflink')).then(d => 102 | t.matchSnapshot(archy(archyize(d)).trim())) 103 | }) 104 | 105 | test('broken json', function (t) { 106 | rpt(path.resolve(fixtures, 'bad'), function (er, d) { 107 | t.ok(d.error, 'Got an error object') 108 | t.equal(d.error && d.error.code, 'EJSONPARSE') 109 | t.ok(d, 'Got a tree') 110 | t.end() 111 | }) 112 | }) 113 | 114 | test('missing json does not obscure deeper errors', function (t) { 115 | rpt(path.resolve(fixtures, 'empty'), function (er, d) { 116 | var error = d.error 117 | t.ok(error, 'Error reading json of top level') 118 | t.equal(error && error.code, 'ENOENT') 119 | var childError = d.children.length===1 && d.children[0].error 120 | t.ok(childError, 'Error parsing JSON of child node') 121 | t.equal(childError && childError.code, 'EJSONPARSE') 122 | t.end() 123 | }) 124 | }) 125 | 126 | test('missing folder', function (t) { 127 | rpt(path.resolve(fixtures, 'does-not-exist'), function (er, d) { 128 | t.ok(er, 'Got an error object') 129 | t.equal(er && er.code, 'ENOENT') 130 | t.ok(!d, 'No tree on top level error') 131 | t.end() 132 | }) 133 | }) 134 | 135 | test('missing symlinks', function (t) { 136 | rpt(path.resolve(fixtures, 'badlink'), function (er, d) { 137 | if (er && er.code !== 'ENOENT') throw er 138 | t.is(d.children.length, 2, 'both broken children are included') 139 | d.children.forEach(function (child) { 140 | t.ok(child.error, 'Child node has an error') 141 | }) 142 | t.end() 143 | }) 144 | }) 145 | 146 | function archyize (d, seen) { 147 | seen = seen || {} 148 | var path = d.realpath 149 | 150 | var label = !Object.keys(d.package).length ? '' : 151 | d.package._id ? d.package._id + ' ' : 152 | d.name ? d.name + (d.package.version ? '@' + d.package.version : '') + ' ' : 153 | d.package.name ? d.package.name + (d.package.version ? '@' + d.package.version : '') + ' ' : 154 | '' 155 | label += path.substr(cwd.length + 1) 156 | 157 | if (d . target) { 158 | return { label: label + ' (symlink)', nodes: [] } 159 | } 160 | 161 | return { 162 | label: label, 163 | nodes: d.children.map(function (kid) { 164 | return archyize(kid, seen) 165 | }) 166 | } 167 | } 168 | 169 | test('realpath gutchecks', t => { 170 | const d = path.resolve(cwd, 'test/fixtures') 171 | const realpath = require('../realpath.js') 172 | const {realpathSync} = fs 173 | Object.keys(symlinks).map(link => t.test(link, t => 174 | realpath( 175 | path.resolve(d, link), 176 | new Map(), 177 | new Map(), 178 | 0 179 | ).then( 180 | real => t.equal(real, realpathSync(path.resolve(d, link))), 181 | er => t.throws(()=> realpathSync(path.resolve(d, link))) 182 | ))) 183 | t.end() 184 | }) 185 | 186 | test('cleanup', function (t) { 187 | cleanup() 188 | t.end() 189 | }) 190 | -------------------------------------------------------------------------------- /rpt.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | /* istanbul ignore next */ 3 | const promisify = require('util').promisify || require('util-promisify') 4 | const { resolve, basename, dirname, join } = require('path') 5 | const rpj = promisify(require('read-package-json')) 6 | const readdir = promisify(require('readdir-scoped-modules')) 7 | const realpath = require('./realpath.js') 8 | 9 | let ID = 0 10 | class Node { 11 | constructor (pkg, logical, physical, er, cache) { 12 | // should be impossible. 13 | const cached = cache.get(physical) 14 | /* istanbul ignore next */ 15 | if (cached && !cached.then) 16 | throw new Error('re-creating already instantiated node') 17 | 18 | cache.set(physical, this) 19 | 20 | const parent = basename(dirname(logical)) 21 | if (parent.charAt(0) === '@') 22 | this.name = `${parent}/${basename(logical)}` 23 | else 24 | this.name = basename(logical) 25 | this.path = logical 26 | this.realpath = physical 27 | this.error = er 28 | this.id = ID++ 29 | this.package = pkg || {} 30 | this.parent = null 31 | this.isLink = false 32 | this.children = [] 33 | } 34 | } 35 | 36 | class Link extends Node { 37 | constructor (pkg, logical, physical, realpath, er, cache) { 38 | super(pkg, logical, physical, er, cache) 39 | 40 | // if the target has started, but not completed, then 41 | // a Promise will be in the cache to indicate this. 42 | const cachedTarget = cache.get(realpath) 43 | if (cachedTarget && cachedTarget.then) 44 | cachedTarget.then(node => { 45 | this.target = node 46 | this.children = node.children 47 | }) 48 | 49 | this.target = cachedTarget || new Node(pkg, logical, realpath, er, cache) 50 | this.realpath = realpath 51 | this.isLink = true 52 | this.error = er 53 | this.children = this.target.children 54 | } 55 | } 56 | 57 | // this is the way it is to expose a timing issue which is difficult to 58 | // test otherwise. The creation of a Node may take slightly longer than 59 | // the creation of a Link that targets it. If the Node has _begun_ its 60 | // creation phase (and put a Promise in the cache) then the Link will 61 | // get a Promise as its cachedTarget instead of an actual Node object. 62 | // This is not a problem, because it gets resolved prior to returning 63 | // the tree or attempting to load children. However, it IS remarkably 64 | // difficult to get to happen in a test environment to verify reliably. 65 | // Hence this kludge. 66 | const newNode = (pkg, logical, physical, er, cache) => 67 | process.env._TEST_RPT_SLOW_LINK_TARGET_ === '1' 68 | ? new Promise(res => setTimeout(() => 69 | res(new Node(pkg, logical, physical, er, cache)), 10)) 70 | : new Node(pkg, logical, physical, er, cache) 71 | 72 | const loadNode = (logical, physical, cache, rpcache, stcache) => { 73 | // cache temporarily holds a promise placeholder so we 74 | // don't try to create the same node multiple times. 75 | // this is very rare to encounter, given the aggressive 76 | // caching on fs.realpath and fs.lstat calls, but 77 | // it can happen in theory. 78 | const cached = cache.get(physical) 79 | /* istanbul ignore next */ 80 | if (cached) 81 | return Promise.resolve(cached) 82 | 83 | const p = realpath(physical, rpcache, stcache, 0).then(real => 84 | rpj(join(real, 'package.json')) 85 | .then(pkg => [pkg, null], er => [null, er]) 86 | .then(([pkg, er]) => 87 | physical === real ? newNode(pkg, logical, physical, er, cache) 88 | : new Link(pkg, logical, physical, real, er, cache) 89 | ), 90 | // if the realpath fails, don't bother with the rest 91 | er => new Node(null, logical, physical, er, cache)) 92 | 93 | cache.set(physical, p) 94 | return p 95 | } 96 | 97 | const loadChildren = (node, cache, filterWith, rpcache, stcache) => { 98 | // if a Link target has started, but not completed, then 99 | // a Promise will be in the cache to indicate this. 100 | // 101 | // XXX When we can one day loadChildren on the link *target* instead of 102 | // the link itself, to match real dep resolution, then we may end up with 103 | // a node target in the cache that isn't yet done resolving when we get 104 | // here. For now, though, this line will never be reached, so it's hidden 105 | // 106 | // if (node.then) 107 | // return node.then(node => loadChildren(node, cache, filterWith, rpcache, stcache)) 108 | 109 | const nm = join(node.path, 'node_modules') 110 | return realpath(nm, rpcache, stcache, 0) 111 | .then(rm => readdir(rm).then(kids => [rm, kids])) 112 | .then(([rm, kids]) => Promise.all( 113 | kids.filter(kid => 114 | kid.charAt(0) !== '.' && (!filterWith || filterWith(node, kid))) 115 | .map(kid => loadNode(join(nm, kid), join(rm, kid), cache, rpcache, stcache))) 116 | ).then(kidNodes => { 117 | kidNodes.forEach(k => k.parent = node) 118 | node.children.push.apply(node.children, kidNodes.sort((a, b) => 119 | (a.package.name ? a.package.name.toLowerCase() : a.path) 120 | .localeCompare( 121 | (b.package.name ? b.package.name.toLowerCase() : b.path) 122 | ))) 123 | return node 124 | }) 125 | .catch(() => node) 126 | } 127 | 128 | const loadTree = (node, did, cache, filterWith, rpcache, stcache) => { 129 | // impossible except in pathological ELOOP cases 130 | /* istanbul ignore next */ 131 | if (did.has(node.realpath)) 132 | return Promise.resolve(node) 133 | 134 | did.add(node.realpath) 135 | 136 | // load children on the target, not the link 137 | return loadChildren(node, cache, filterWith, rpcache, stcache) 138 | .then(node => Promise.all( 139 | node.children 140 | .filter(kid => !did.has(kid.realpath)) 141 | .map(kid => loadTree(kid, did, cache, filterWith, rpcache, stcache)) 142 | )).then(() => node) 143 | } 144 | 145 | // XXX Drop filterWith and/or cb in next semver major bump 146 | const rpt = (root, filterWith, cb) => { 147 | if (!cb && typeof filterWith === 'function') { 148 | cb = filterWith 149 | filterWith = null 150 | } 151 | 152 | const cache = new Map() 153 | // we can assume that the cwd is real enough 154 | const cwd = process.cwd() 155 | const rpcache = new Map([[ cwd, cwd ]]) 156 | const stcache = new Map() 157 | const p = realpath(root, rpcache, stcache, 0) 158 | .then(realRoot => loadNode(root, realRoot, cache, rpcache, stcache)) 159 | .then(node => loadTree(node, new Set(), cache, filterWith, rpcache, stcache)) 160 | 161 | if (typeof cb === 'function') 162 | p.then(tree => cb(null, tree), cb) 163 | 164 | return p 165 | } 166 | 167 | rpt.Node = Node 168 | rpt.Link = Link 169 | module.exports = rpt 170 | -------------------------------------------------------------------------------- /tap-snapshots/test-basic.js-TAP.test.js: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/basic.js TAP deeproot > deeproot tree 1`] = ` 9 | root@1.2.3 test/fixtures/root 10 | ├─┬ @scope/x@1.2.3 test/fixtures/root/node_modules/@scope/x 11 | │ └─┬ glob@4.0.5 test/fixtures/root/node_modules/@scope/x/node_modules/glob 12 | │ ├── graceful-fs@3.0.2 test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/graceful-fs 13 | │ ├── inherits@2.0.1 test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/inherits 14 | │ ├─┬ minimatch@1.0.0 test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/minimatch 15 | │ │ ├── lru-cache@2.5.0 test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/minimatch/node_modules/lru-cache 16 | │ │ └── sigmund@1.0.0 test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/minimatch/node_modules/sigmund 17 | │ └── once@1.3.0 test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/once 18 | ├── @scope/y@1.2.3 test/fixtures/root/node_modules/@scope/y 19 | └─┬ foo@1.2.3 test/fixtures/root/node_modules/foo 20 | └── abbrev@1.1.1 test/fixtures/root/node_modules/foo/node_modules/express 21 | ` 22 | 23 | exports[`test/basic.js TAP filterWith > undefined 1`] = ` 24 | root@1.2.3 test/fixtures/root 25 | ├── @scope/x@1.2.3 test/fixtures/root/node_modules/@scope/x 26 | ├── @scope/y@1.2.3 test/fixtures/root/node_modules/@scope/y 27 | └── foo@1.2.3 test/fixtures/root/node_modules/foo 28 | ` 29 | 30 | exports[`test/basic.js TAP linkedroot > linkedroot tree 1`] = ` 31 | root@1.2.3 test/fixtures/root 32 | ├─┬ @scope/x@1.2.3 test/fixtures/root/node_modules/@scope/x 33 | │ └─┬ glob@4.0.5 test/fixtures/root/node_modules/@scope/x/node_modules/glob 34 | │ ├── graceful-fs@3.0.2 test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/graceful-fs 35 | │ ├── inherits@2.0.1 test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/inherits 36 | │ ├─┬ minimatch@1.0.0 test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/minimatch 37 | │ │ ├── lru-cache@2.5.0 test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/minimatch/node_modules/lru-cache 38 | │ │ └── sigmund@1.0.0 test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/minimatch/node_modules/sigmund 39 | │ └── once@1.3.0 test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/once 40 | ├── @scope/y@1.2.3 test/fixtures/root/node_modules/@scope/y 41 | └─┬ foo@1.2.3 test/fixtures/root/node_modules/foo 42 | └── abbrev@1.1.1 test/fixtures/root/node_modules/foo/node_modules/express 43 | ` 44 | 45 | exports[`test/basic.js TAP looking outside of cwd > undefined 1`] = ` 46 | root@1.2.3 test/fixtures/root 47 | ├─┬ @scope/x@1.2.3 test/fixtures/root/node_modules/@scope/x 48 | │ └─┬ glob@4.0.5 test/fixtures/root/node_modules/@scope/x/node_modules/glob 49 | │ ├── graceful-fs@3.0.2 test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/graceful-fs 50 | │ ├── inherits@2.0.1 test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/inherits 51 | │ ├─┬ minimatch@1.0.0 test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/minimatch 52 | │ │ ├── lru-cache@2.5.0 test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/minimatch/node_modules/lru-cache 53 | │ │ └── sigmund@1.0.0 test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/minimatch/node_modules/sigmund 54 | │ └── once@1.3.0 test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/once 55 | ├── @scope/y@1.2.3 test/fixtures/root/node_modules/@scope/y 56 | └─┬ foo@1.2.3 test/fixtures/root/node_modules/foo 57 | └── abbrev@1.1.1 test/fixtures/root/node_modules/foo/node_modules/express 58 | ` 59 | 60 | exports[`test/basic.js TAP noname > noname tree 1`] = ` 61 | test/fixtures/noname 62 | └── test/fixtures/noname/node_modules/foo 63 | ` 64 | 65 | exports[`test/basic.js TAP other > other tree 1`] = ` 66 | test/fixtures/other 67 | └── glob@4.0.5 test/fixtures/root/node_modules/@scope/x/node_modules/glob (symlink) 68 | ` 69 | 70 | exports[`test/basic.js TAP root > root tree 1`] = ` 71 | root@1.2.3 test/fixtures/root 72 | ├─┬ @scope/x@1.2.3 test/fixtures/root/node_modules/@scope/x 73 | │ └─┬ glob@4.0.5 test/fixtures/root/node_modules/@scope/x/node_modules/glob 74 | │ ├── graceful-fs@3.0.2 test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/graceful-fs 75 | │ ├── inherits@2.0.1 test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/inherits 76 | │ ├─┬ minimatch@1.0.0 test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/minimatch 77 | │ │ ├── lru-cache@2.5.0 test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/minimatch/node_modules/lru-cache 78 | │ │ └── sigmund@1.0.0 test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/minimatch/node_modules/sigmund 79 | │ └── once@1.3.0 test/fixtures/root/node_modules/@scope/x/node_modules/glob/node_modules/once 80 | ├── @scope/y@1.2.3 test/fixtures/root/node_modules/@scope/y 81 | └─┬ foo@1.2.3 test/fixtures/root/node_modules/foo 82 | └── abbrev@1.1.1 test/fixtures/root/node_modules/foo/node_modules/express 83 | ` 84 | 85 | exports[`test/basic.js TAP selflink > selflink tree 1`] = ` 86 | selflink@1.2.3 test/fixtures/selflink 87 | ├── @scope/y@1.2.3 test/fixtures/selflink/node_modules/@scope/y 88 | ├─┬ @scope/z@1.2.3 test/fixtures/selflink/node_modules/@scope/z 89 | │ └── glob@4.0.5 test/fixtures/selflink/node_modules/foo/node_modules/glob (symlink) 90 | └─┬ foo@1.2.3 test/fixtures/selflink/node_modules/foo 91 | ├─┬ glob@4.0.5 test/fixtures/selflink/node_modules/foo/node_modules/glob 92 | │ ├── graceful-fs@3.0.2 test/fixtures/selflink/node_modules/foo/node_modules/glob/node_modules/graceful-fs 93 | │ ├── inherits@2.0.1 test/fixtures/selflink/node_modules/foo/node_modules/glob/node_modules/inherits 94 | │ ├─┬ minimatch@1.0.0 test/fixtures/selflink/node_modules/foo/node_modules/glob/node_modules/minimatch 95 | │ │ ├── lru-cache@2.5.0 test/fixtures/selflink/node_modules/foo/node_modules/glob/node_modules/minimatch/node_modules/lru-cache 96 | │ │ └── sigmund@1.0.0 test/fixtures/selflink/node_modules/foo/node_modules/glob/node_modules/minimatch/node_modules/sigmund 97 | │ └── once@1.3.0 test/fixtures/selflink/node_modules/foo/node_modules/glob/node_modules/once 98 | └── selflink@1.2.3 test/fixtures/selflink (symlink) 99 | ` 100 | 101 | exports[`test/basic.js TAP shake out Link target timing issue > undefined 1`] = ` 102 | selflink@1.2.3 test/fixtures/selflink 103 | ├── @scope/y@1.2.3 test/fixtures/selflink/node_modules/@scope/y 104 | ├─┬ @scope/z@1.2.3 test/fixtures/selflink/node_modules/@scope/z 105 | │ └── glob@4.0.5 test/fixtures/selflink/node_modules/foo/node_modules/glob (symlink) 106 | └─┬ foo@1.2.3 test/fixtures/selflink/node_modules/foo 107 | ├─┬ glob@4.0.5 test/fixtures/selflink/node_modules/foo/node_modules/glob 108 | │ ├── graceful-fs@3.0.2 test/fixtures/selflink/node_modules/foo/node_modules/glob/node_modules/graceful-fs 109 | │ ├── inherits@2.0.1 test/fixtures/selflink/node_modules/foo/node_modules/glob/node_modules/inherits 110 | │ ├─┬ minimatch@1.0.0 test/fixtures/selflink/node_modules/foo/node_modules/glob/node_modules/minimatch 111 | │ │ ├── lru-cache@2.5.0 test/fixtures/selflink/node_modules/foo/node_modules/glob/node_modules/minimatch/node_modules/lru-cache 112 | │ │ └── sigmund@1.0.0 test/fixtures/selflink/node_modules/foo/node_modules/glob/node_modules/minimatch/node_modules/sigmund 113 | │ └── once@1.3.0 test/fixtures/selflink/node_modules/foo/node_modules/glob/node_modules/once 114 | └── selflink@1.2.3 test/fixtures/selflink (symlink) 115 | ` 116 | --------------------------------------------------------------------------------