dependency-tree 11.4.3 → 11.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,18 +10,17 @@
10
10
  npm install dependency-tree
11
11
  ```
12
12
 
13
- * Works for JS (AMD, CommonJS, ES6 modules), TypeScript, and CSS preprocessors (CSS (PostCSS), Sass, Stylus, and Less); basically, any module type supported by [precinct](https://github.com/dependents/node-precinct).
14
- - For CommonJS modules, 3rd party dependencies (npm installed dependencies) are included in the tree by default
15
- - Dependency path resolutions are handled by [filing-cabinet](https://github.com/dependents/node-filing-cabinet)
16
- - Supports RequireJS and Webpack loaders
17
- * All core Node modules (assert, path, fs, etc) are removed from the dependency list by default
13
+ * Supports JS (AMD, CommonJS, ES6), TypeScript, and CSS preprocessors (PostCSS, Sass, Stylus, Less) - any type handled by [precinct](https://github.com/dependents/node-precinct)
14
+ - CommonJS: third-party (npm) dependencies are included by default
15
+ - Path resolution is handled by [filing-cabinet](https://github.com/dependents/node-filing-cabinet); RequireJS and webpack loaders are supported
16
+ * Core Node built-ins (assert, path, fs, etc.) are excluded by default
18
17
 
19
18
  ## Usage
20
19
 
21
20
  ```js
22
21
  const dependencyTree = require('dependency-tree');
23
22
 
24
- // Returns a dependency tree object for the given file
23
+ // Returns a nested dependency tree object for the given file
25
24
  const tree = dependencyTree({
26
25
  filename: 'path/to/a/file',
27
26
  directory: 'path/to/all/files',
@@ -31,14 +30,13 @@ const tree = dependencyTree({
31
30
  nodeModulesConfig: {
32
31
  entry: 'module'
33
32
  }, // optional
34
- filter: path => path.indexOf('node_modules') === -1, // optional
33
+ filter: path => !path.includes('node_modules'), // optional
35
34
  nonExistent: [], // optional
36
35
  noTypeDefinitions: false // optional
37
36
  });
38
37
 
39
- // Returns a post-order traversal (list form) of the tree with duplicate sub-trees pruned.
40
- // This is useful for bundling source files, because the list gives the concatenation order.
41
- // Note: you can pass the same arguments as you would to dependencyTree()
38
+ // Returns a post-order flat list of absolute paths (dependencies before dependents).
39
+ // Useful as a concatenation order for bundling.
42
40
  const list = dependencyTree.toList({
43
41
  filename: 'path/to/a/file',
44
42
  directory: 'path/to/all/files'
@@ -47,78 +45,68 @@ const list = dependencyTree.toList({
47
45
 
48
46
  ### Options
49
47
 
50
- * `requireConfig`: path to a requirejs config for AMD modules (allows for the result of aliased module paths)
51
- * `webpackConfig`: path to a webpack config for aliased modules
52
- * `tsConfig`: path to a typescript config (or a preloaded object representing the typescript config)
53
- * `tsConfigPath`: a (virtual) path to typescript config file when `tsConfig` option is given as an object, not a string. Needed to calculate [Path Mapping](https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping). If not given when `tsConfig` is an object, **Path Mapping** is ignored. This is not needed when `tsConfig` is given as a path string.
54
- * `nodeModulesConfig`: config for resolving entry file for node_modules
55
- * `visited`: object used for avoiding redundant subtree generations via memoization.
56
- * `nonExistent`: array used for storing the list of partial paths that do not exist
57
- * `filter`: a function used to determine if a module (and its subtree) should be included in the dependency tree
58
- - The first argument given to the filter is an absolute filepath to the dependency and the second is the filepath to the currently traversed file. Should return a `Boolean`. If it returns `true`, the module is included in the resulting tree.
59
- * `detective`: object with configuration specific to detectives used to find dependencies of a file
60
- - for example `detective.amd.skipLazyLoaded: true` tells the AMD detective to omit inner requires
61
- - See [precinct's usage docs](https://github.com/dependents/node-precinct#usage) for the list of module types you can pass options to.
62
- * `noTypeDefinitions`: For TypeScript imports, whether to resolve to `*.js` instead of `*.d.ts`.
63
-
64
- ### Format Details
65
-
66
- The object form is a mapping of the dependency tree to the filesystem –
67
- where every key is an absolute filepath and the value is another object/subtree.
68
-
69
- Example:
48
+ | Option | Type | Default | Description |
49
+ |---|---|---|---|
50
+ | `filename` | `string` | - | **Required.** Absolute path to the entry file |
51
+ | `directory` | `string` | - | **Required.** Root directory used to resolve relative paths |
52
+ | `requireConfig` | `string` | `undefined` | Path to a RequireJS config for AMD modules (resolves aliased paths) |
53
+ | `webpackConfig` | `string` | `undefined` | Path to a webpack config for aliased modules |
54
+ | `tsConfig` | `string \| object` | `undefined` | Path to a TypeScript config file, or a preloaded config object |
55
+ | `tsConfigPath` | `string` | `undefined` | Virtual path for the TypeScript config when `tsConfig` is an object. Required for [Path Mapping](https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping); ignored when `tsConfig` is a string path |
56
+ | `nodeModulesConfig` | `object` | `undefined` | Config for resolving `node_modules` entry files (e.g. `{ entry: 'module' }`) |
57
+ | `visited` | `object` | `{}` | Memoization cache (`filename → subtree`) to skip already-processed files |
58
+ | `nonExistent` | `string[]` | `[]` | Array populated with partial paths that could not be resolved |
59
+ | `filter` | `(depPath, parentPath) => boolean` | `undefined` | Return `true` to include a dependency (and its subtree) in the tree |
60
+ | `detective` | `object` | `undefined` | Detector options passed to [precinct](https://github.com/dependents/node-precinct#usage) - e.g. `{ amd: { skipLazyLoaded: true } }` |
61
+ | `noTypeDefinitions` | `boolean` | `false` | Resolve TypeScript imports to `*.js` instead of `*.d.ts` |
62
+
63
+ ### Output format
64
+
65
+ The default output is a nested object where every key is an absolute file path and the value is its own subtree:
70
66
 
71
67
  ```js
72
68
  {
73
- '/Users/mrjoelkemp/Documents/node-dependency-tree/test/example/extended/a.js': {
74
- '/Users/mrjoelkemp/Documents/node-dependency-tree/test/example/extended/b.js': {
75
- '/Users/mrjoelkemp/Documents/node-dependency-tree/test/example/extended/d.js': {},
76
- '/Users/mrjoelkemp/Documents/node-dependency-tree/test/example/extended/e.js': {}
69
+ '/path/to/a.js': {
70
+ '/path/to/b.js': {
71
+ '/path/to/d.js': {},
72
+ '/path/to/e.js': {}
77
73
  },
78
- '/Users/mrjoelkemp/Documents/node-dependency-tree/test/example/extended/c.js': {
79
- '/Users/mrjoelkemp/Documents/node-dependency-tree/test/example/extended/f.js': {},
80
- '/Users/mrjoelkemp/Documents/node-dependency-tree/test/example/extended/g.js': {}
74
+ '/path/to/c.js': {
75
+ '/path/to/f.js': {},
76
+ '/path/to/g.js': {}
81
77
  }
82
78
  }
83
79
  }
84
80
  ```
85
81
 
86
- This structure was chosen to serve as a visual representation of the dependency tree
87
- for use in the [Dependents](https://github.com/mrjoelkemp/sublime-dependents) plugin.
82
+ This format was designed for visual representation in the [Dependents](https://github.com/mrjoelkemp/sublime-dependents) plugin.
88
83
 
89
- ### CLI version
84
+ ### CLI
90
85
 
91
- * Assumes a global install: `npm install -g dependency-tree`
86
+ Requires a global install: `npm install -g dependency-tree`
92
87
 
93
88
  ```
94
- dependency-tree --directory=path/to/all/supported/files [--list-form] [-c path/to/require/config] [-w path/to/webpack/config] filename
89
+ dependency-tree --directory=path/to/files [--list-form] [--es6-mixed-imports] [-c path/to/require/config] [-w path/to/webpack/config] filename
95
90
  ```
96
91
 
97
- Prints the dependency tree of the given filename as stringified json (by default).
98
-
99
- * You can alternatively print out the list form one element per line using the `--list-form` option.
100
-
101
- ## How does this work?
92
+ Prints the dependency tree as JSON. Use `--list-form` to print one path per line instead.
102
93
 
103
- Dependency tree takes in a starting file, extracts its declared dependencies via [precinct](https://github.com/dependents/node-precinct/), resolves each of those dependencies to a file on the filesystem via [filing-cabinet](https://github.com/dependents/node-filing-cabinet), then recursively performs those steps until there are no more dependencies to process.
94
+ ## How it works
104
95
 
105
- In more detail, the starting file is passed to precinct to extract dependencies. Dependency-tree doesn't care about how to extract dependencies, so it delegates that work to precinct: which is a multi-language dependency extractor; we'll focus on JavaScript tree generation for this example. To do the extraction, precinct delegates the abstract-syntax-tree (AST) generation to the default parser for [node-source-walk](https://github.com/dependents/node-source-walk). Precinct uses the AST to determine what type of JS module the file is (Commonjs, AMD, or ES6) and then delegates to the "detective" that's appropriate for that module type. The "detective" contains the logic for how to extract dependencies based on the module syntax format; i.e., the way dependencies are declared in commonjs is different than in AMD (which has 4 ways of doing that, for example).
96
+ dependency-tree passes the entry file to [precinct](https://github.com/dependents/node-precinct/) to extract raw dependency strings, then passes each to [filing-cabinet](https://github.com/dependents/node-filing-cabinet) to resolve them to real filesystem paths, and recurses until the full tree is built.
106
97
 
107
- After using the detective to get the (raw, like './foobar') dependency strings, precinct passes that back to dependency-tree. Of course, in order to find the dependencies in './foobar', we need to resolve that dependency to a real file on the filesystem. To do this, dependency-tree delegates that task to filing-cabinet: which is a multi-language dependency resolver.
98
+ Precinct generates an AST via [node-source-walk](https://github.com/dependents/node-source-walk), detects the module format (CommonJS, AMD, or ES6), and delegates to the appropriate detective to extract dependency declarations.
108
99
 
109
- Filing-cabinet reuses (for performance) the AST that precinct made node-source-walk generate. It then does a similar check on the AST to see which module type (commonjs, amd, or es6) is being used in the file (again, we're assuming a regular JS file for this example) and then delegates to the appropriate resolver for that module type. We need different resolvers because a dependency name in AMD could be aliased via a requirejs config. Similarly, commonjs has its own algorithm for resolving dependencies.
110
-
111
- So after the appropriate resolver finds the file on the filesystem, filing-cabinet has successfully mapped a raw dependency name to a file on the filesystem. Now, dependency-tree has a file that it can also traverse (repeating exactly what was done for the starting file).
112
-
113
- At the end of traversing every file (in a depth-first fashion), we have a fully populated dependency tree. :dancers:
100
+ Filing-cabinet reuses that AST to detect the module format again, then delegates to the right resolver - necessary because AMD supports path aliasing via a RequireJS config, while CommonJS has its own resolution algorithm. The resolver returns an absolute path, which dependency-tree then recurses into.
114
101
 
115
102
  ## FAQ
116
103
 
117
- ### Why aren't some some dependencies being detected?
104
+ ### Why aren't some dependencies being detected?
105
+
106
+ Bugs in [precinct](https://github.com/dependents/node-precinct) or an incomplete `requireConfig`/`webpackConfig`/`tsConfig` are the most common causes. Any path that could not be resolved is appended to the array passed as `nonExistent`.
118
107
 
119
- If there are bugs in [precinct](https://github.com/dependents/node-precinct) or if the `requireConfig`/`webpackConfig`/`tsConfig` options are incomplete,
120
- some dependencies may not be resolved. The optional array passed to the `nonExistent` option will be populated with paths
121
- that could not be resolved. You can check this array to see where problems might exist.
108
+ For detailed resolution logs, set `NODE_DEBUG=*` when using the CLI:
122
109
 
123
- You can also use the `NODE_DEBUG=*` env variable along with the cli version to see debugging information explaining where resolution went wrong.
124
- Example: `NODE_DEBUG=* dependency-tree -w path/to/webpack.config.json path/to/a/file`
110
+ ```sh
111
+ NODE_DEBUG=* dependency-tree -w path/to/webpack.config.json path/to/a/file
112
+ ```
package/bin/cli.js CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  'use strict';
4
4
 
5
+ const process = require('node:process');
5
6
  const { program } = require('commander');
7
+ const { stringifyChunked } = require('@discoveryjs/json-ext');
6
8
  const dependencyTree = require('../index.js');
7
9
  const { name, description, version } = require('../package.json');
8
10
 
@@ -16,6 +18,7 @@ program
16
18
  .option('-c, --require-config <path>', 'path to a requirejs config')
17
19
  .option('-w, --webpack-config <path>', 'path to a webpack config')
18
20
  .option('-t, --ts-config <path>', 'path to a typescript config')
21
+ .option('--es6-mixed-imports', 'detect dependencies from files that mix ES6 imports and CJS require() calls')
19
22
  .option('--list-form', 'output the list form of the tree (one element per line)')
20
23
  .showHelpAfterError()
21
24
  .parse();
@@ -26,7 +29,12 @@ const options = {
26
29
  root: cliOptions.directory,
27
30
  config: cliOptions.requireConfig,
28
31
  webpackConfig: cliOptions.webpackConfig,
29
- tsConfig: cliOptions.tsConfig
32
+ tsConfig: cliOptions.tsConfig,
33
+ detective: {
34
+ es6: {
35
+ mixedImports: Boolean(cliOptions.es6MixedImports)
36
+ }
37
+ }
30
38
  };
31
39
 
32
40
  if (cliOptions.listForm) {
@@ -38,5 +46,9 @@ if (cliOptions.listForm) {
38
46
  } else {
39
47
  const tree = dependencyTree(options);
40
48
 
41
- console.log(JSON.stringify(tree));
49
+ for (const chunk of stringifyChunked(tree)) {
50
+ process.stdout.write(chunk);
51
+ }
52
+
53
+ process.stdout.write('\n');
42
54
  }
package/index.js CHANGED
@@ -10,22 +10,21 @@ const Config = require('./lib/config.js');
10
10
  const debug = debuglog('tree');
11
11
 
12
12
  /**
13
- * Recursively find all dependencies (avoiding circular) traversing the entire dependency tree
14
- * and returns a flat list of all unique, visited nodes
13
+ * Returns the dependency tree of a module as a nested object
15
14
  *
16
15
  * @param {Object} options
17
- * @param {String} options.filename - The path of the module whose tree to traverse
18
- * @param {String} options.directory - The directory containing all JS files
19
- * @param {String} [options.requireConfig] - The path to a requirejs config
20
- * @param {String} [options.webpackConfig] - The path to a webpack config
21
- * @param {String} [options.nodeModulesConfig] - config for resolving entry file for node_modules
22
- * @param {Object} [options.visited] - Cache of visited, absolutely pathed files that should not be reprocessed.
23
- * Format is a filename -> tree as list lookup table
24
- * @param {Array} [options.nonExistent] - List of partials that do not exist
25
- * @param {Boolean} [options.isListForm=false]
26
- * @param {String|Object} [options.tsConfig] Path to a typescript config (or a preloaded one).
27
- * @param {Boolean} [options.noTypeDefinitions] For TypeScript imports, whether to resolve to `*.js` instead of `*.d.ts`.
28
- * @return {Object}
16
+ * @param {string} options.filename - Entry module path
17
+ * @param {string} options.directory - Root directory containing all files
18
+ * @param {string} [options.requireConfig] - Path to a RequireJS config
19
+ * @param {string} [options.webpackConfig] - Path to a webpack config
20
+ * @param {string} [options.nodeModulesConfig] - Config for resolving node_modules entry files
21
+ * @param {Object} [options.visited] - Memoization cache: filename ? subtree
22
+ * @param {Array} [options.nonExistent] - Accumulator for unresolvable partials
23
+ * @param {boolean} [options.isListForm=false] - Return a flat list instead of a tree
24
+ * @param {string|Object} [options.tsConfig] - Path to (or preloaded) TypeScript config
25
+ * @param {string} [options.tsConfigPath] - (Virtual) path to tsconfig when tsConfig is an object; needed for Path Mapping
26
+ * @param {boolean} [options.noTypeDefinitions] - Resolve TS imports to `*.js` instead of `*.d.ts`
27
+ * @returns {Object}
29
28
  */
30
29
  module.exports = function(options = {}) {
31
30
  const config = new Config(options);
@@ -56,30 +55,23 @@ module.exports = function(options = {}) {
56
55
  };
57
56
 
58
57
  /**
59
- * Executes a post-order depth first search on the dependency tree and returns a
60
- * list of absolute file paths. The order of files in the list will be the
61
- * proper concatenation order for bundling.
58
+ * Returns a post-order flat list of absolute file paths (dependencies before dependents).
59
+ * Every file's dependencies appear at lower indices, so the root entry point is last.
60
+ * The list contains no duplicates. Accepts the same options as the default export.
62
61
  *
63
- * In other words, for any file in the list, all of that file's dependencies (direct or indirect) will appear at
64
- * lower indices in the list. The root (entry point) file will therefore appear last.
65
- *
66
- * The list will not contain duplicates.
67
- *
68
- * Params are those of module.exports
62
+ * @param {Object} options - Same as the default export
63
+ * @returns {Array<string>}
69
64
  */
70
65
  module.exports.toList = function(options = {}) {
71
- options.isListForm = true;
72
-
73
- return module.exports(options);
66
+ return module.exports({ ...options, isListForm: true });
74
67
  };
75
68
 
76
69
  /**
77
- * Returns the list of dependencies for the given filename
78
- *
79
- * Protected for testing
70
+ * Returns resolved dependency paths for the file described by `config`.
71
+ * Exposed for testing.
80
72
  *
81
- * @param {Config} config
82
- * @return {Array}
73
+ * @param {Config} config
74
+ * @returns {Array<string>}
83
75
  */
84
76
  module.exports._getDependencies = function(config = {}) {
85
77
  const precinctOptions = config.detectiveConfig;
@@ -132,8 +124,8 @@ module.exports._getDependencies = function(config = {}) {
132
124
  };
133
125
 
134
126
  /**
135
- * @param {Config} config
136
- * @return {Object|Set}
127
+ * @param {Config} config
128
+ * @returns {Object|Set}
137
129
  */
138
130
  function traverse(config = {}) {
139
131
  const subTree = config.isListForm ? new Set() : {};
@@ -148,8 +140,7 @@ function traverse(config = {}) {
148
140
  let dependencies = module.exports._getDependencies(config);
149
141
 
150
142
  debug('cabinet-resolved all dependencies: ', dependencies);
151
- // Prevents cycles by eagerly marking the current file as read
152
- // so that any dependent dependencies exit
143
+ // Eagerly mark the current file before recursing so any re-entrant visit exits early
153
144
  config.visited[config.filename] = config.isListForm ? [] : {};
154
145
 
155
146
  if (config.filter) {
@@ -163,7 +154,7 @@ function traverse(config = {}) {
163
154
  for (const dependency of dependencies) {
164
155
  const localConfig = config.clone();
165
156
  localConfig.filename = dependency;
166
- localConfig.directory = getDirectory(localConfig);
157
+ localConfig.directory = getLocalConfigDirectory(localConfig);
167
158
 
168
159
  if (localConfig.isListForm) {
169
160
  for (const item of traverse(localConfig)) {
@@ -184,7 +175,7 @@ function traverse(config = {}) {
184
175
  return subTree;
185
176
  }
186
177
 
187
- // Mutate the list input to do a dereferenced modification of the user-supplied list
178
+ // Dedupe in-place so the caller's array reference stays valid
188
179
  function dedupeNonExistent(nonExistent) {
189
180
  const deduped = new Set(nonExistent);
190
181
  nonExistent.length = deduped.size;
@@ -196,24 +187,35 @@ function dedupeNonExistent(nonExistent) {
196
187
  }
197
188
  }
198
189
 
199
- // If the file is in a node module, use the root directory of the module
200
- function getDirectory(localConfig) {
201
- if (!localConfig.filename.includes('node_modules')) {
202
- return localConfig.directory;
190
+ // If the file is in a node_modules directory, we want to resolve the root of the package,
191
+ // not the file itself, since the file may be buried in a subdirectory and not contain all
192
+ // of the package's dependencies
193
+ function getLocalConfigDirectory(localConfig) {
194
+ const { filename, directory } = localConfig;
195
+
196
+ if (!filename.includes('node_modules')) {
197
+ return directory;
203
198
  }
204
199
 
205
- return getProjectPath(path.dirname(localConfig.filename)) || localConfig.directory;
206
- }
200
+ const dir = path.dirname(filename);
201
+ const parts = dir.split('node_modules');
207
202
 
208
- function getProjectPath(filename) {
209
- try {
210
- const nodeModuleParts = filename.split('node_modules');
211
- const packageSubPathPath = nodeModuleParts.pop().split(path.sep).filter(Boolean);
212
- const packageName = packageSubPathPath[0].startsWith('@') ? `${packageSubPathPath[0]}${path.sep}${packageSubPathPath[1]}` : packageSubPathPath[0];
213
-
214
- return path.normalize([...nodeModuleParts, `${path.sep}${packageName}`].join('node_modules'));
215
- } catch {
216
- debug(`Could not determine the root directory of package file ${filename}. Using default`);
217
- return null;
203
+ if (parts.length < 2) {
204
+ return directory;
218
205
  }
206
+
207
+ const afterNodeModules = parts.pop();
208
+ if (!afterNodeModules) {
209
+ return directory;
210
+ }
211
+
212
+ const segments = afterNodeModules.split(path.sep).filter(Boolean);
213
+ if (segments.length === 0) {
214
+ return directory;
215
+ }
216
+
217
+ const packageName = segments[0].startsWith('@') ? `${segments[0]}${path.sep}${segments[1]}` : segments[0];
218
+ const projectPath = path.normalize(`${parts.join('node_modules')}node_modules${path.sep}${packageName}`);
219
+
220
+ return projectPath;
219
221
  }
package/lib/config.js CHANGED
@@ -27,6 +27,7 @@ module.exports = class Config {
27
27
  if (this.filter && typeof this.filter !== 'function') throw new Error('filter must be a function');
28
28
 
29
29
  if (typeof this.tsConfig === 'string') {
30
+ // Pre-parse once so all recursive clones share the object form
30
31
  debug('preparsing the ts config into an object for performance');
31
32
  const ts = require('typescript');
32
33
  const tsParsedConfig = ts.readJsonConfigFile(this.tsConfig, ts.sys.readFile);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dependency-tree",
3
- "version": "11.4.3",
3
+ "version": "11.5.0",
4
4
  "description": "Get the dependency tree of a module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -49,9 +49,10 @@
49
49
  "node": ">=18"
50
50
  },
51
51
  "dependencies": {
52
+ "@discoveryjs/json-ext": "^1.1.0",
52
53
  "commander": "^12.1.0",
53
- "filing-cabinet": "^5.3.0",
54
- "precinct": "^12.3.1",
54
+ "filing-cabinet": "^5.5.1",
55
+ "precinct": "^12.3.2",
55
56
  "typescript": "^5.9.3"
56
57
  },
57
58
  "devDependencies": {