Graph traversal to walk through ESM dependency graph for further static analysis. The traversal algorithm is classified as Breadth-first search (BFS).
$ npm install deps-walker
Here is an example of an entry point module entry.js
with its dependencies, which in turn depend on their dependencies, which in turn depend on...
//------ entry.js ------
import a from './a.js';
import b from './b.js';
//------ a.js ------
import b from './b.js';
import c from './c.js';
import d from './d.js';
//------ c.js ------
import d from './d.js';
//------ d.js ------
import b from './b.js';
In other words:
entry.js -> a.js
entry.js -> b.js
a.js -> b.js
a.js -> c.js
a.js -> d.js
c.js -> d.js
d.js -> b.js
deps-walker
is used to traverse entry.js
dependency graph:
const walk = require('deps-walker')();
walk('entry.js', (err, data) => {
if (err) {
// catch any errors...
return;
}
const { filePath, dependencies } = data;
// analyse module dependencies
});
The dependencies are traversed in the following order:
deps-walker
support async/await API, it can be used to await traverse completion:
async function traverse() {
await walk('entry.js', (err, data) => {
/*...*/
});
console.log('Traverse is completed');
}
deps-walker
supports multiple roots:
walk(['entry1.js', 'entry2.js', 'entry3.js'], (err, data) => {
/*...*/
});
deps-walker
uses @babel/parser with sourceType: 'module'
option by default. You can specify any other available options:
const babelParse = require('deps-walker/lib/parsers/babel');
const walk = require('deps-walker')({
parse: (...args) =>
babelParse(...args, {
// options
sourceType: 'module',
plugins: ['jsx', 'flow']
})
});
or specify your own parse
implementation:
const walk = require('deps-walker')({
parse: (code, filePath) => {
// parse implementation
}
});
It is not always obvious where import x from 'module'
should look to find the file behind module, it depends on module resolution algorithms, which are specific for module bundlers, module syntax specs, etc.. deps-walker
uses resolve package, which implements NodeJS module resolution behavior. You may configure NodeJS resolve
via available options:
const nodejsResolve = require('deps-walker/lib/resolvers/nodejs');
const walk = require('deps-walker')({
resolve: (...args) =>
nodejsResolve(...args, {
// options
extensions: ['.js'],
paths: ['rootDir'],
moduleDirectory: 'node_modules'
})
});
You can also use other module resolution algorithms:
const walk = require('deps-walker')({
resolve: async (filePath, contextPath) => {
// resolve implementation
}
});
You may break traversal for some dependencies by specifying ignore
function:
const walk = require('deps-walker')({
// ignore node_modules
ignore: filePath => /node_modules/.test(filePath)
});
Module parsing and resolving can be resource intensive operation (CPU, I/O), cache allows you to speed up consecutive runs:
const cache = require('deps-walker/cache');
const walk = require('deps-walker')({ cache });
//...
await cache.load('./cache.json');
await walk('entry.js', (err, data) => {
/*...*/
});
await cache.save('./cache.json');
You can also override the default file reader:
const fsPromises = require('fs').promises;
const read = _.memoize(filePath => fsPromises.readFile(filePath, 'utf8'));
const walk = require('deps-walker')({ read });