Skip to content

Instantly share code, notes, and snippets.

@vikaspotluri123
Created January 24, 2022 00:17
Show Gist options
  • Save vikaspotluri123/cf6b7940b248364c36e7084f5cd94d2f to your computer and use it in GitHub Desktop.
Save vikaspotluri123/cf6b7940b248364c36e7084f5cd94d2f to your computer and use it in GitHub Desktop.
ESM import tree generator

Node ESM Tracer

This gist leverages Node's experimental loader hooks to create a dependency tree of your app. This can be useful to find circular dependencies, or diagnose race conditions / import order issues. Note: Experimental Loader Hooks are, well, experimental! This has been validated to run against Node v16.13.0

Here's the output of running NODE_NO_WARNINGS=1 node --experimental-loader=./loader-tracer.js index.js with this gist:

index.js
  (node{,_modules} excluded)
  a.js
    b.js
      d.js
        [circular] a.js
    d.js
  b.js
  c.js
    d.js (truncated)
import B from './b.js';
import D from './d.js'
export default 'A';
import D from './d.js'
export default 'D';
import D from './d.js';
export default 'C';
import a from './a.js';
export default 'D';
import path from 'path';
import A from './a.js';
import B from './b.js';
import C from './c.js';
export default 'index';
import path from 'path';
const SPACE_CHAR = ' ';
const SPACE_RPT = 2;
const TRUNCATE_SEEN_TREES = true;
const EXCLUDE_NODE_AND_NODE_MODULES = true;
class LoaderTracer {
constructor() {
this.wd = process.cwd();
this.dependencies = {};
this.root = null;
}
trackDependency(resolved, parentUrl) {
// If the parentUrl is undefined, it's because it's being loaded by the entrypoint
if (!parentUrl) {
this.root = resolved.url;
return;
}
const key = parentUrl;
this.dependencies[key] = this.dependencies[key] || [];
this.dependencies[key].push(resolved.url);
}
shortKey(source) {
if (!source.startsWith('file://')) {
if (!source.startsWith('node:')) {
// @todo - find a case where this happens and handle it!
debugger;
}
return source;
}
const cleanPath = new URL(source).pathname;
return path.relative(this.wd, cleanPath);
}
spaces(depth) {
return SPACE_CHAR.repeat(depth * SPACE_RPT);
}
serializeDependency(key, tree = [], history = [], depth = 0) {
const dependencies = this.dependencies[key];
let response = `${this.spaces(depth)}${this.shortKey(key)}\n`;
// Leaf dependencies don't have any other dependencies
if (!dependencies) {
return response;
}
const localTree = [...tree, key];
let nodeModuleTextIncluded = false;
for (const dependency of dependencies) {
if (EXCLUDE_NODE_AND_NODE_MODULES && (dependency.includes('node_modules') || dependency.includes('node:'))) {
if (!nodeModuleTextIncluded) {
response += `${this.spaces(depth + 1)}(node{,_modules} excluded)\n`;
nodeModuleTextIncluded = true;
}
continue;
}
// CASE: we've seen this import in the import chain, which means this is a circular dependency
if (localTree.includes(dependency)) {
response += `${this.spaces(depth + 1)}[circular] ${this.shortKey(dependency)}\n`;
// CASE: we've printed the dependency tree of this import before, so we can truncate it (if configured to)
} else if (TRUNCATE_SEEN_TREES && history.includes(dependency)) {
const truncatedText = this.dependencies[dependencies]?.length > 0 ? ' (truncated)' : '';
response += `${this.spaces(depth + 1)}${this.shortKey(dependency)}${truncatedText}\n`;
// Normal case - recursively build the dependency tree, and track it as printed
} else {
response += this.serializeDependency(dependency, localTree, history, depth + 1);
history.push(dependency);
}
}
return response;
}
toString() {
return this.serializeDependency(this.root);
}
}
const tracer = new LoaderTracer();
export async function resolve(specifier, context, defaultResolve) {
const response = await defaultResolve(specifier, context, defaultResolve);
tracer.trackDependency(response, context.parentURL);
return response;
}
global.__importDependencyTracer = tracer;
global.__dumpImportDependencyTree = () => console.log(tracer.toString());
process.on('beforeExit', global.__dumpImportDependencyTree);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment