├── .gitignore ├── test ├── stubs │ ├── simple2.js │ ├── parent │ │ ├── greatgrandchild.js │ │ ├── child2.js │ │ ├── grandchild.js │ │ ├── child1.js │ │ └── parent.js │ ├── simple.js │ ├── dotdot │ │ └── dotdot.js │ ├── circular │ │ ├── circle-a.js │ │ ├── circle-b.js │ │ └── circle-c.js │ ├── circular2 │ │ ├── circle-a.js │ │ ├── circle-b.js │ │ └── circle-c.js │ └── uses_node_modules.js └── mainTest.js ├── CHANGELOG.md ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── package.json ├── LICENSE ├── README.md └── main.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /test/stubs/simple2.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/stubs/parent/greatgrandchild.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/stubs/simple.js: -------------------------------------------------------------------------------- 1 | const test = require("./simple2"); -------------------------------------------------------------------------------- /test/stubs/dotdot/dotdot.js: -------------------------------------------------------------------------------- 1 | const test = require("../simple2") -------------------------------------------------------------------------------- /test/stubs/circular/circle-a.js: -------------------------------------------------------------------------------- 1 | const circleB = require("./circle-b"); -------------------------------------------------------------------------------- /test/stubs/circular/circle-b.js: -------------------------------------------------------------------------------- 1 | const circleC = require("./circle-c"); -------------------------------------------------------------------------------- /test/stubs/circular/circle-c.js: -------------------------------------------------------------------------------- 1 | const circleA = require("./circle-a"); -------------------------------------------------------------------------------- /test/stubs/circular2/circle-a.js: -------------------------------------------------------------------------------- 1 | const circleB = require("./circle-b"); -------------------------------------------------------------------------------- /test/stubs/circular2/circle-b.js: -------------------------------------------------------------------------------- 1 | const circleC = require("./circle-c"); -------------------------------------------------------------------------------- /test/stubs/circular2/circle-c.js: -------------------------------------------------------------------------------- 1 | const circleA = require("./circle-b"); -------------------------------------------------------------------------------- /test/stubs/parent/child2.js: -------------------------------------------------------------------------------- 1 | const test = require("./grandchild"); 2 | 3 | module.exports = {}; -------------------------------------------------------------------------------- /test/stubs/parent/grandchild.js: -------------------------------------------------------------------------------- 1 | const test = require("./greatgrandchild.js"); 2 | module.exports = {}; -------------------------------------------------------------------------------- /test/stubs/parent/child1.js: -------------------------------------------------------------------------------- 1 | const test2 = require("./grandchild"); 2 | const lodash = require("lodash"); 3 | 4 | module.exports = {}; -------------------------------------------------------------------------------- /test/stubs/uses_node_modules.js: -------------------------------------------------------------------------------- 1 | const is = require("@sindresorhus/is"); 2 | const child1 = require("./parent/child1.js"); 3 | 4 | module.exports = {}; -------------------------------------------------------------------------------- /test/stubs/parent/parent.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const test = require("./child1"); 3 | const test2 = require("./child2"); 4 | 5 | module.exports = {}; -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v4.0.0 4 | 5 | - Upgrade major dep `@11ty/eleventy-utils@2` with **Node 18+** minimum bump 6 | - Adds npm provenance 7 | 8 | ## v3.0.1 9 | 10 | - Milestone: https://github.com/11ty/eleventy-dependency-tree/milestone/1?closed=1 11 | - Add support for `pnpm` #3 12 | - Ignore tests for npm publish #4 13 | 14 | ## v3.0.0 15 | 16 | - All paths returned from this utility are normalized to POSIX-format by default. -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches-ignore: 4 | - "gh-pages" 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: ["ubuntu-latest", "macos-latest", "windows-latest"] 11 | node: ["18", "20", "22", "24"] 12 | name: Node.js ${{ matrix.node }} on ${{ matrix.os }} 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Setup node 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: ${{ matrix.node }} 19 | - run: npm install 20 | - run: npm test 21 | env: 22 | YARN_GPG: no 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release to npm 2 | on: 3 | release: 4 | types: [published] 5 | permissions: read-all 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | environment: GitHub Publish 10 | permissions: 11 | contents: read 12 | id-token: write 13 | steps: 14 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # 4.1.7 15 | - uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # 4.0.3 16 | with: 17 | node-version: "22" 18 | registry-url: 'https://registry.npmjs.org' 19 | - run: npm install -g npm@latest 20 | - run: npm ci 21 | - run: npm test 22 | - if: ${{ github.event.release.tag_name != '' && env.NPM_PUBLISH_TAG != '' }} 23 | run: npm publish --provenance --access=public --tag=${{ env.NPM_PUBLISH_TAG }} 24 | env: 25 | NPM_PUBLISH_TAG: ${{ contains(github.event.release.tag_name, '-beta.') && 'beta' || 'latest' }} 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@11ty/dependency-tree", 3 | "version": "4.0.1", 4 | "description": "Finds all JavaScript CommmonJS require() dependencies from a filename.", 5 | "main": "main.js", 6 | "files": [ 7 | "main.js", 8 | "!test", 9 | "!test/**" 10 | ], 11 | "scripts": { 12 | "test": "npx ava" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/11ty/eleventy-dependency-tree.git" 17 | }, 18 | "author": { 19 | "name": "Zach Leatherman", 20 | "email": "zach@zachleat.com", 21 | "url": "https://zachleat.com/" 22 | }, 23 | "license": "MIT", 24 | "ava": { 25 | "files": [ 26 | "./test/*.js" 27 | ], 28 | "watchMode": { 29 | "ignoreChanged": [ 30 | "./test/stubs/**" 31 | ] 32 | } 33 | }, 34 | "devDependencies": { 35 | "@sindresorhus/is": "^4.6.0", 36 | "ava": "^6.4.1" 37 | }, 38 | "dependencies": { 39 | "@11ty/eleventy-utils": "^2.0.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Zach Leatherman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/mainTest.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const { TemplatePath } = require("@11ty/eleventy-utils"); 3 | const DependencyTree = require("../main.js"); 4 | 5 | test("Nonexistent", t => { 6 | t.throws(() => { 7 | DependencyTree("./test/stubs/thisdoesnotexist.js"); 8 | }); 9 | }); 10 | 11 | test("Allow not Found", t => { 12 | DependencyTree("./test/stubs/thisdoesnotexist.js", { allowNotFound: true }); 13 | t.true(true); 14 | }); 15 | 16 | test("Not require()’d before calling", t => { 17 | DependencyTree("./test/stubs/parent/parent.js"); 18 | t.true(true); 19 | }); 20 | 21 | test("simple.js", t => { 22 | t.deepEqual(DependencyTree("./test/stubs/simple.js"), ["./test/stubs/simple2.js"]); 23 | }); 24 | 25 | test("parent.js", t => { 26 | t.deepEqual(DependencyTree("./test/stubs/parent/parent.js").sort(), [ 27 | "./test/stubs/parent/child1.js", 28 | "./test/stubs/parent/child2.js", 29 | "./test/stubs/parent/grandchild.js", 30 | "./test/stubs/parent/greatgrandchild.js" 31 | ]); 32 | 33 | t.deepEqual(DependencyTree("./test/stubs/parent/child1.js").sort(), [ 34 | "./test/stubs/parent/grandchild.js", 35 | "./test/stubs/parent/greatgrandchild.js" 36 | ]); 37 | 38 | t.deepEqual(DependencyTree("./test/stubs/parent/child2.js").sort(), [ 39 | "./test/stubs/parent/grandchild.js", 40 | "./test/stubs/parent/greatgrandchild.js" 41 | ]); 42 | 43 | t.deepEqual(DependencyTree("./test/stubs/parent/grandchild.js"), [ 44 | "./test/stubs/parent/greatgrandchild.js" 45 | ]); 46 | 47 | t.deepEqual(DependencyTree("./test/stubs/parent/greatgrandchild.js"), []); 48 | }); 49 | 50 | test("circular", t => { 51 | t.deepEqual(DependencyTree("./test/stubs/circular/circle-a.js").sort(), [ 52 | "./test/stubs/circular/circle-b.js", 53 | "./test/stubs/circular/circle-c.js"]); 54 | }); 55 | 56 | test("another circular", t => { 57 | t.deepEqual(DependencyTree("./test/stubs/circular2/circle-a.js").sort(), [ 58 | "./test/stubs/circular2/circle-b.js", 59 | "./test/stubs/circular2/circle-c.js" 60 | ]); 61 | }); 62 | 63 | test("dot dot (dependency is up a directory)", t => { 64 | t.deepEqual(DependencyTree("./test/stubs/dotdot/dotdot.js").sort(), [ 65 | "./test/stubs/simple2.js" 66 | ]); 67 | }); 68 | 69 | test("only node_modules", t => { 70 | t.deepEqual(DependencyTree("./test/stubs/uses_node_modules.js", { 71 | nodeModuleNamesOnly: true 72 | }).sort(), [ 73 | "@sindresorhus/is", 74 | "lodash", 75 | ]); 76 | }); 77 | 78 | test("getNodeModuleName", t => { 79 | let p1 = TemplatePath.normalizeOperatingSystemFilePath("./eleventy-dependency-tree/node_modules/lodash/lodash.js") 80 | t.is(DependencyTree.getNodeModuleName(p1), "lodash"); 81 | 82 | let p2 = TemplatePath.normalizeOperatingSystemFilePath("./eleventy-dependency-tree/node_modules/@sindresorhus/is/dist/index.js") 83 | t.is(DependencyTree.getNodeModuleName(p2), "@sindresorhus/is"); 84 | }); 85 | 86 | test("both files and node_modules", t => { 87 | t.deepEqual(DependencyTree("./test/stubs/uses_node_modules.js", { 88 | nodeModuleNames: "include" 89 | }).sort(), [ 90 | "./test/stubs/parent/child1.js", 91 | "./test/stubs/parent/grandchild.js", 92 | "./test/stubs/parent/greatgrandchild.js", 93 | "@sindresorhus/is", 94 | "lodash", 95 | ]); 96 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `@11ty/dependency-tree` 2 | 3 | Returns an unordered array of local paths to dependencies of a CommonJS node JavaScript file (everything it or any of its dependencies `require`s). 4 | 5 | * See also: [`@11ty/dependency-tree-esm`](https://github.com/11ty/eleventy-utils/tree/main/parse-deps-esm) for the ESM version. 6 | * See also: [`@11ty/dependency-tree-typescript`](https://github.com/11ty/eleventy-utils/tree/main/parse-deps-typescript) for the TypeScript version. 7 | 8 | Reduced feature (faster) alternative to the [`dependency-tree` package](https://www.npmjs.com/package/dependency-tree). This is used by Eleventy to find dependencies of a JavaScript file to watch for changes to re-run Eleventy’s build. 9 | 10 | ## Big Huge Caveat 11 | 12 | ⚠ A big caveat to this plugin is that it will require the file in order to build a dependency tree. So if your module has side effects and you don’t want it to execute—do not use this! 13 | 14 | ## Installation 15 | 16 | ``` 17 | npm install --save-dev @11ty/dependency-tree 18 | ``` 19 | 20 | ## Features 21 | 22 | * Ignores `node_modules` 23 | * Or, use `nodeModuleNames` to control whether or not `node_modules` package names are included (added in v2.0.1) 24 | * Ignores Node’s built-ins (e.g. `path`) 25 | * Handles circular dependencies (Node does this too) 26 | 27 | ## Usage 28 | 29 | ```js 30 | // my-file.js 31 | 32 | // if my-local-dependency.js has dependencies, it will include those too 33 | const test = require("./my-local-dependency.js"); 34 | 35 | // ignored, is a built-in 36 | const path = require("path"); 37 | ``` 38 | 39 | ```js 40 | const DependencyTree = require("@11ty/dependency-tree"); 41 | 42 | DependencyTree("./my-file.js"); 43 | // returns ["./my-local-dependency.js"] 44 | ``` 45 | 46 | ### `allowNotFound` 47 | 48 | ```js 49 | const DependencyTree = require("@11ty/dependency-tree"); 50 | 51 | DependencyTree("./this-does-not-exist.js"); // throws an error 52 | 53 | DependencyTree("./this-does-not-exist.js", { allowNotFound: true }); 54 | // returns [] 55 | ``` 56 | 57 | ### `nodeModuleNames` 58 | 59 | (Added in v2.0.1) Controls whether or not node package names are included in the list of dependencies. 60 | 61 | * `nodeModuleNames: "include"`: included alongside the local JS files. 62 | * `nodeModuleNames: "exclude"` (default): node module package names are excluded. 63 | * `nodeModuleNames: "only"`: only node module package names are returned. 64 | 65 | ```js 66 | // my-file.js: 67 | 68 | require("./my-local-dependency.js"); 69 | require("@11ty/eleventy"); 70 | ``` 71 | 72 | ```js 73 | const DependencyTree = require("@11ty/dependency-tree"); 74 | 75 | DependencyTree("./my-file.js"); 76 | // returns ["./my-local-dependency.js"] 77 | 78 | DependencyTree("./my-file.js", { nodeModuleNames: "exclude" }); 79 | // returns ["./my-local-dependency.js"] 80 | 81 | DependencyTree("./my-file.js", { nodeModuleNames: "include" }); 82 | // returns ["./my-local-dependency.js", "@11ty/eleventy"] 83 | 84 | DependencyTree("./my-file.js", { nodeModuleNames: "only" }); 85 | // returns ["@11ty/eleventy"] 86 | ``` 87 | 88 | #### (Deprecated) `nodeModuleNamesOnly` 89 | 90 | (Added in v2.0.0) Changed to use `nodeModuleNames` option instead. Backwards compatibility is maintained automatically. 91 | 92 | * `nodeModuleNamesOnly: false` is mapped to `nodeModuleNames: "exclude"` 93 | * `nodeModuleNamesOnly: true` is mapped to `nodeModuleNames: "only"` 94 | 95 | If both `nodeModuleNamesOnly` and `nodeModuleNames` are included in options, `nodeModuleNames` takes precedence. 96 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { TemplatePath } = require("@11ty/eleventy-utils"); 3 | 4 | function getAbsolutePath(filename) { 5 | let normalizedFilename = path.normalize(filename); // removes dot slash 6 | let hasDotSlash = filename.startsWith("./"); 7 | return hasDotSlash ? path.join(path.resolve("."), normalizedFilename) : normalizedFilename; 8 | } 9 | 10 | function getRelativePath(filename) { 11 | let normalizedFilename = path.normalize(filename); // removes dot slash 12 | let workingDirectory = path.resolve("."); 13 | let result = "./" + (normalizedFilename.startsWith(workingDirectory) ? normalizedFilename.substr(workingDirectory.length + 1) : normalizedFilename); 14 | return result; 15 | } 16 | 17 | function getNodeModuleName(filename) { 18 | let foundNodeModules = false; 19 | let moduleName = []; 20 | 21 | let s = filename.split(path.sep); 22 | for(let entry of s) { 23 | if(entry === '.pnpm') { 24 | foundNodeModules = false; 25 | } 26 | 27 | if(foundNodeModules) { 28 | moduleName.push(entry); 29 | if(!entry.startsWith("@")) { 30 | return moduleName.join("/"); 31 | } 32 | } 33 | 34 | if(entry === "node_modules") { 35 | foundNodeModules = true; 36 | } 37 | } 38 | 39 | return false; 40 | } 41 | 42 | /* unordered */ 43 | function getDependenciesFor(filename, avoidCircular, optionsArg = {}) { 44 | // backwards compatibility with `nodeModuleNamesOnly` boolean option 45 | // Using `nodeModuleNames` property moving forward 46 | if(("nodeModuleNamesOnly" in optionsArg) && !("nodeModuleNames" in optionsArg)) { 47 | if(optionsArg.nodeModuleNamesOnly === true) { 48 | optionsArg.nodeModuleNames = "only"; 49 | } 50 | if(optionsArg.nodeModuleNamesOnly === false) { 51 | optionsArg.nodeModuleNames = "exclude"; 52 | } 53 | } 54 | 55 | let options = Object.assign({ 56 | allowNotFound: false, 57 | nodeModuleNames: "exclude", // also "include" or "only" 58 | }, optionsArg); 59 | let absoluteFilename = getAbsolutePath(filename) 60 | 61 | try { 62 | require(absoluteFilename); 63 | } catch(e) { 64 | if(e.code === "MODULE_NOT_FOUND" && options.allowNotFound) { 65 | // do nothing 66 | } else { 67 | throw e; 68 | } 69 | } 70 | 71 | 72 | let mod; 73 | for(let entry in require.cache) { 74 | if(entry === absoluteFilename) { 75 | mod = require.cache[entry]; 76 | break; 77 | } 78 | } 79 | 80 | let dependencies = new Set(); 81 | 82 | if(!mod) { 83 | if(!options.allowNotFound) { 84 | throw new Error(`Could not find ${filename} in @11ty/dependency-tree`); 85 | } 86 | } else { 87 | let relativeFilename = getRelativePath(mod.filename); 88 | if(!avoidCircular) { 89 | avoidCircular = {}; 90 | } else if(options.nodeModuleNames !== "only") { 91 | dependencies.add(relativeFilename); 92 | } 93 | 94 | avoidCircular[relativeFilename] = true; 95 | 96 | if(mod.children) { 97 | for(let child of mod.children) { 98 | let relativeChildFilename = getRelativePath(child.filename); 99 | let nodeModuleName = getNodeModuleName(child.filename); 100 | 101 | if(options.nodeModuleNames !== "exclude" && nodeModuleName) { 102 | dependencies.add(nodeModuleName); 103 | } 104 | // Add dependencies of this dependency (not top level node_modules) 105 | if(nodeModuleName === false) { 106 | if(!dependencies.has(relativeChildFilename) && // avoid infinite looping with circular deps 107 | !avoidCircular[relativeChildFilename] ) { 108 | for(let dependency of getDependenciesFor(relativeChildFilename, avoidCircular, options)) { 109 | dependencies.add(dependency); 110 | } 111 | } 112 | } 113 | } 114 | } 115 | } 116 | 117 | return dependencies; 118 | } 119 | 120 | function getCleanDependencyListFor(filename, options = {}) { 121 | let deps = Array.from( getDependenciesFor(filename, null, options) ); 122 | 123 | return deps.map(filePath => { 124 | if(filePath.startsWith("./")) { 125 | return TemplatePath.standardizeFilePath(filePath); 126 | } 127 | return filePath; // node_module name 128 | }); 129 | } 130 | 131 | module.exports = getCleanDependencyListFor; 132 | module.exports.getNodeModuleName = getNodeModuleName; --------------------------------------------------------------------------------