Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20, 22, 24]
node: [20, 22, 24]

steps:
- name: Clone repository
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ jobs:

- name: Lint
run: npm run lint

- name: Check generated types are up to date
run: npm run check:types
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ npm install dependency-tree
## Usage

```js
const dependencyTree = require('dependency-tree');
// ESM
import dependencyTree from 'dependency-tree';
// CommonJS
const { default: dependencyTree } = require('dependency-tree');

// Returns a nested dependency tree object for the given file
const tree = dependencyTree({
Expand All @@ -30,7 +33,7 @@ const tree = dependencyTree({
nodeModulesConfig: {
entry: 'module'
}, // optional
filter: path => !path.includes('node_modules'), // optional
filter: (dependencyPath, parentPath) => !dependencyPath.includes('node_modules'), // optional
nonExistent: [], // optional
noTypeDefinitions: false // optional
});
Expand All @@ -49,15 +52,19 @@ const list = dependencyTree.toList({
|---|---|---|---|
| `filename` | `string` | - | **Required.** Absolute path to the entry file |
| `directory` | `string` | - | **Required.** Root directory used to resolve relative paths |
| `root` | `string` | `undefined` | Alias for `directory` |
| `requireConfig` | `string` | `undefined` | Path to a RequireJS config for AMD modules (resolves aliased paths) |
| `config` | `string` | `undefined` | Alias for `requireConfig` |
| `webpackConfig` | `string` | `undefined` | Path to a webpack config for aliased modules |
| `tsConfig` | `string \| object` | `undefined` | Path to a TypeScript config file, or a preloaded config object |
| `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 |
| `nodeModulesConfig` | `object` | `undefined` | Config for resolving `node_modules` entry files (e.g. `{ entry: 'module' }`) |
| `visited` | `object` | `{}` | Memoization cache (`filename subtree`) to skip already-processed files |
| `visited` | `object` | `{}` | Memoization cache (filename to subtree) to skip already-processed files |
| `nonExistent` | `string[]` | `[]` | Array populated with partial paths that could not be resolved |
| `filter` | `(depPath, parentPath) => boolean` | `undefined` | Return `true` to include a dependency (and its subtree) in the tree |
| `detective` | `object` | `undefined` | Detective options passed to [precinct](https://github.com/dependents/node-precinct#usage) - e.g. `{ amd: { skipLazyLoaded: true } }`, `{ ts: { skipTypeImports: true } }` |
| `isListForm` | `boolean` | `false` | Return a flat post-order list of paths instead of a nested tree (same as calling `dependencyTree.toList()`) |
| `filter` | `(dependencyPath: string, parentPath: string) => boolean` | `undefined` | Return `true` to include a dependency (and its subtree) in the tree |
| `detectiveConfig` | `object` | `{}` | Options passed to [precinct](https://github.com/dependents/node-precinct#usage) for dependency extraction - e.g. `{ amd: { skipLazyLoaded: true } }`, `{ ts: { skipTypeImports: true } }` |
| `detective` | `object` | `{}` | Alias for `detectiveConfig` |
| `noTypeDefinitions` | `boolean` | `false` | Resolve TypeScript imports to `*.js` instead of `*.d.ts` |

### Output format
Expand Down
12 changes: 6 additions & 6 deletions bin/cli.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
#!/usr/bin/env node

'use strict';
import process from 'node:process';
import { stringifyChunked } from '@discoveryjs/json-ext';
import { program } from 'commander';
import dependencyTree from '../index.js';
import pkg from '../package.json' with { type: 'json' };

const process = require('node:process');
const { program } = require('commander');
const { stringifyChunked } = require('@discoveryjs/json-ext');
const dependencyTree = require('../index.js');
const { name, description, version } = require('../package.json');
const { name, description, version } = pkg;

program
.name(name)
Expand Down
34 changes: 0 additions & 34 deletions index.d.ts

This file was deleted.

65 changes: 30 additions & 35 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,19 @@
'use strict';

const fs = require('node:fs');
const path = require('node:path');
const { debuglog } = require('node:util');
const cabinet = require('filing-cabinet');
const precinct = require('precinct');
const Config = require('./lib/config.js');
import fs from 'node:fs';
import path from 'node:path';
import { debuglog } from 'node:util';
import cabinet from 'filing-cabinet';
import precinct from 'precinct';
import Config from './lib/config.js';

const debug = debuglog('tree');

/**
* Returns the dependency tree of a module as a nested object
*
* @param {Object} options
* @param {string} options.filename - Entry module path
* @param {string} options.directory - Root directory containing all files
* @param {string} [options.requireConfig] - Path to a RequireJS config
* @param {string} [options.webpackConfig] - Path to a webpack config
* @param {string} [options.nodeModulesConfig] - Config for resolving node_modules entry files
* @param {Object} [options.visited] - Memoization cache: filename ? subtree
* @param {Array} [options.nonExistent] - Accumulator for unresolvable partials
* @param {boolean} [options.isListForm=false] - Return a flat list instead of a tree
* @param {string|Object} [options.tsConfig] - Path to (or preloaded) TypeScript config
* @param {string} [options.tsConfigPath] - (Virtual) path to tsconfig when tsConfig is an object; needed for Path Mapping
* @param {boolean} [options.noTypeDefinitions] - Resolve TS imports to `*.js` instead of `*.d.ts`
* @returns {Object}
* @param {import('./lib/config.js').ConfigOptions} [options]
* @returns {object}
*/
module.exports = function(options = {}) {
function dependencyTree(options = {}) {
const config = new Config(options);

if (!fs.existsSync(config.filename)) {
Expand All @@ -53,36 +40,35 @@ module.exports = function(options = {}) {

debug('final tree', tree);
return tree;
};
}

/**
* Returns a post-order flat list of absolute file paths (dependencies before dependents).
* Every file's dependencies appear at lower indices, so the root entry point is last.
* The list contains no duplicates. Accepts the same options as the default export.
*
* @param {Object} options - Same as the default export
* @returns {Array<string>}
* @param {Parameters<typeof dependencyTree>[0]} options - Same as the default export
* @returns {string[]}
*/
module.exports.toList = function(options = {}) {
return module.exports({ ...options, isListForm: true });
dependencyTree.toList = function(options = {}) {
return dependencyTree({ ...options, isListForm: true });
};

/**
* Returns resolved dependency paths for the file described by `config`.
* Exposed for testing.
*
* @param {Config} config
* @returns {Array<string>}
* @returns {string[]}
*/
module.exports._getDependencies = function(config = {}) {
function getDependencies(config) {
const precinctOptions = config.detectiveConfig;
precinctOptions.includeCore = false;
let dependencies;

try {
dependencies = precinct.paperwork(config.filename, precinctOptions);
debug(`extracted ${dependencies.length} dependencies: `, dependencies);
} catch (error) {
} catch(error) {
debug(`error getting dependencies: ${error.message}`);
debug(error.stack);
return [];
Expand Down Expand Up @@ -122,13 +108,13 @@ module.exports._getDependencies = function(config = {}) {
}

return resolvedDependencies;
};
}

/**
* @param {Config} config
* @returns {Object|Set}
* @returns {Object|Set<string>}
*/
function traverse(config = {}) {
function traverse(config) {
const subTree = config.isListForm ? new Set() : {};

debug(`traversing ${config.filename}`);
Expand All @@ -138,7 +124,7 @@ function traverse(config = {}) {
return config.visited[config.filename];
}

let dependencies = module.exports._getDependencies(config);
let dependencies = getDependencies(config);

debug('cabinet-resolved all dependencies: ', dependencies);
// Eagerly mark the current file before recursing so any re-entrant visit exits early
Expand Down Expand Up @@ -177,6 +163,9 @@ function traverse(config = {}) {
}

// Dedupe in-place so the caller's array reference stays valid
/**
* @param {string[]} nonExistent
*/
function dedupeNonExistent(nonExistent) {
const deduped = new Set(nonExistent);
nonExistent.length = deduped.size;
Expand All @@ -191,6 +180,10 @@ function dedupeNonExistent(nonExistent) {
// If the file is in a node_modules directory, we want to resolve the root of the package,
// not the file itself, since the file may be buried in a subdirectory and not contain all
// of the package's dependencies
/**
* @param {Config} localConfig
* @returns {string}
*/
function getLocalConfigDirectory(localConfig) {
const { filename, directory } = localConfig;

Expand Down Expand Up @@ -220,3 +213,5 @@ function getLocalConfigDirectory(localConfig) {

return projectPath;
}

export default dependencyTree;
46 changes: 35 additions & 11 deletions lib/config.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,46 @@
'use strict';
import path from 'node:path';
import process from 'node:process';
import { debuglog } from 'node:util';
import { createRequire } from 'node:module';

const path = require('node:path');
const process = require('node:process');
const { debuglog } = require('node:util');
const require = createRequire(import.meta.url);

const debug = debuglog('tree');

module.exports = class Config {
/**
* @typedef {object} ConfigOptions
* @property {string} [filename] - Entry module path
* @property {string} [directory] - Root directory containing all files
* @property {string} [root] - Alias for `directory`
* @property {string} [requireConfig] - Path to a RequireJS config for AMD modules
* @property {string} [config] - Alias for `requireConfig`
* @property {string} [webpackConfig] - Path to a webpack config for aliased modules
* @property {Record<string, unknown>} [nodeModulesConfig] - Config for resolving node_modules entry files
* @property {Record<string, object>} [visited] - Memoization cache: filename to subtree
* @property {string[]} [nonExistent] - Accumulator for unresolvable partials
* @property {boolean} [isListForm] - Return a flat list instead of a tree
* @property {string | Record<string, unknown>} [tsConfig] - Path to (or preloaded) TypeScript config
* @property {string} [tsConfigPath] - Virtual path for the tsConfig object; required for Path Mapping
* @property {boolean} [noTypeDefinitions] - Resolve TS imports to `*.js` instead of `*.d.ts`
* @property {Record<string, unknown>} [detectiveConfig] - Options passed to precinct for dependency extraction
* @property {Record<string, unknown>} [detective] - Alias for `detectiveConfig`
* @property {(dependencyPath: string, parentPath: string) => boolean} [filter] - Return `true` to include a dependency
*/

export default class Config {
/**
* @param {ConfigOptions} [options]
*/
constructor(options = {}) {
this.filename = options.filename;
this.directory = options.directory || options.root;
this.visited = options.visited || {};
this.nonExistent = options.nonExistent || [];
this.isListForm = options.isListForm;
this.requireConfig = options.config || options.requireConfig;
this.visited = options.visited ?? {};
this.nonExistent = options.nonExistent ?? [];
this.isListForm = options.isListForm ?? false;
this.requireConfig = options.config ?? options.requireConfig;
this.webpackConfig = options.webpackConfig;
this.nodeModulesConfig = options.nodeModulesConfig;
this.detectiveConfig = options.detective || options.detectiveConfig || {};
this.detectiveConfig = options.detective ?? options.detectiveConfig ?? {};
this.tsConfig = options.tsConfig;
this.tsConfigPath = options.tsConfigPath;
this.noTypeDefinitions = options.noTypeDefinitions;
Expand Down Expand Up @@ -47,4 +71,4 @@ module.exports = class Config {
clone() {
return new Config(this);
}
};
}
Loading
Loading