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