Skip to content

Walks through ESM dependencies graph. It's highly configurable ⚙

License

Notifications You must be signed in to change notification settings

sergei-startsev/deps-walker

Repository files navigation

deps-walker

Graph traversal to walk through ESM dependency graph for further static analysis. The traversal algorithm is classified as Breadth-first search (BFS).

Install

$ npm install deps-walker

Usage

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

dependency graph

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:

Breadth-first search traverse

Async/await API

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');
}

Multiple entry points

deps-walker supports multiple roots:

walk(['entry1.js', 'entry2.js', 'entry3.js'], (err, data) => {
  /*...*/
});

Parsers

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
  }
});

Resolvers

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
  }
});

Ignoring

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)
});

Caching

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');

Reading

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 });

License

MIT