Skip to content

Instantly share code, notes, and snippets.

@lolmaus
Created April 3, 2025 14:34
Show Gist options
  • Save lolmaus/e22e4d527db60cc43b4f5e480acb918d to your computer and use it in GitHub Desktop.
Save lolmaus/e22e4d527db60cc43b4f5e480acb918d to your computer and use it in GitHub Desktop.
Script for displaying all imports for a given TypeScript module

What and why

When you use tools like dependency-cruiser and madge to see dependencies of a given module, they only actually used dependencies.

But if your module or any of its dependencies imports from index-files (AKA barrel files), all modules mentioned in those index files get interpreted and executed. A lot of stuff may be going there:

  • Declaration of various constants.
  • Generation of derived constants.
  • Construction of React components.
  • console.logs.
  • Deprecation checks and warning.

All of which may be entirely unrelated to what you're actually imported.

Ever wondered why your unit tests that are so simple that should not take more than 1 millisecond, actually take 10 seconds to run? That's why.

Installation and usage

  1. npm i -D ts-morph

  2. Put the show-deps.ts into the root of your codebase.

  3. Edit entryFilePath and tsConfigPath at the top of the file.

  4. Run:

     NODE_OPTIONS="--max-old-space-size=4096" npx ts-node ./show-deps.ts > deps.txt
    
  5. Wait a few minutes. It may take a long time.

  6. When the command returns, see deps.txt. It might be very large (megabytes).

Limitations

So far, only shows dependencies from your codebase (not from node_modules/).

Disclaimer

This script was entirely vibe-coded with Claude 3.7 Sonnet Thinking. I didn't even read it, but you should!

If it wipes all your data and gives double espresso to your toddler, that's on you!

/* This script was vibe-coded with Claude 3.7 Sonnet Thinking. */
import { Project, SourceFile } from 'ts-morph';
import * as path from 'path';
// EDIT THIS:
const entryFilePath = path.resolve('./src/ui/components/AppPage/useAppPage.ts');
const tsConfigPath = path.resolve('./tsconfig.json');
function traceAllImports(entryFilePath: string, tsConfigPath: string) {
const project = new Project({
tsConfigFilePath: tsConfigPath,
});
// Get project root from tsconfig location
const projectRoot = path.dirname(path.resolve(tsConfigPath));
// Convert to absolute path if it's relative
const absoluteEntryPath = path.isAbsolute(entryFilePath)
? entryFilePath
: path.resolve(process.cwd(), entryFilePath);
const entryFile = project.getSourceFileOrThrow(absoluteEntryPath);
const processedFiles = new Set<string>();
const importMap = new Map<string, Set<string>>();
// Process the entry file and its dependencies
processFileImports(entryFile);
// Get relative path from project root
function getRelativePath(absolutePath: string): string {
return path.relative(projectRoot, absolutePath);
}
return {
// Direct dependencies of the entry file
directDependencies: Array.from(importMap.get(absoluteEntryPath) || [])
.map(getRelativePath),
// All files that would get loaded when importing the entry file
allLoadedFiles: Array.from(processedFiles).map(getRelativePath),
// Function to print the tree
printTree: () => {
// Track modules where we've already traversed all dependencies
const traversedModules = new Set<string>();
printDependencyTree(absoluteEntryPath, importMap, traversedModules);
}
};
function processFileImports(sourceFile: SourceFile): void {
const filePath = sourceFile.getFilePath();
if (processedFiles.has(filePath)) return;
processedFiles.add(filePath);
// Create set for this file's dependencies if it doesn't exist
if (!importMap.has(filePath)) {
importMap.set(filePath, new Set<string>());
}
// Process imports
const imports = sourceFile.getImportDeclarations();
for (const importDecl of imports) {
const moduleSpecifier = importDecl.getModuleSpecifierValue();
const resolvedModule = resolveModulePath(sourceFile, moduleSpecifier);
if (resolvedModule) {
const resolvedPath = resolvedModule.getFilePath();
// Add to dependency set
importMap.get(filePath)?.add(resolvedPath);
// Process the imported file recursively
processFileImports(resolvedModule);
}
}
// Process re-exports (export ... from)
const exportDeclarations = sourceFile.getExportDeclarations();
for (const exportDecl of exportDeclarations) {
const moduleSpecifier = exportDecl.getModuleSpecifierValue();
if (moduleSpecifier) {
const resolvedModule = resolveModulePath(sourceFile, moduleSpecifier);
if (resolvedModule) {
const resolvedPath = resolvedModule.getFilePath();
// Add to dependency set
importMap.get(filePath)?.add(resolvedPath);
// Process the imported file recursively
processFileImports(resolvedModule);
}
}
}
// Explicitly check for index files in the same directory
checkForIndexFile(sourceFile);
}
function checkForIndexFile(sourceFile: SourceFile): void {
const filePath = sourceFile.getFilePath();
const dirPath = path.dirname(filePath);
// Don't process index files themselves to avoid recursion
if (path.basename(filePath).startsWith('index.')) return;
const indexExtensions = ['.ts', '.tsx', '.js', '.jsx'];
for (const ext of indexExtensions) {
const indexPath = path.join(dirPath, `index${ext}`);
const indexFile = project.getSourceFile(indexPath);
if (indexFile && !processedFiles.has(indexPath)) {
// If an index file exists and hasn't been processed, add it as a dependency
importMap.get(filePath)?.add(indexPath);
processFileImports(indexFile);
}
}
}
function resolveModulePath(sourceFile: SourceFile, moduleSpecifier: string): SourceFile | undefined {
try {
// For relative imports
if (moduleSpecifier.startsWith('.')) {
const dirPath = path.dirname(sourceFile.getFilePath());
const potentialPaths = [
// Try direct import
path.resolve(dirPath, `${moduleSpecifier}.ts`),
path.resolve(dirPath, `${moduleSpecifier}.tsx`),
path.resolve(dirPath, `${moduleSpecifier}.js`),
path.resolve(dirPath, `${moduleSpecifier}.jsx`),
// Try as directory with index
path.resolve(dirPath, moduleSpecifier, 'index.ts'),
path.resolve(dirPath, moduleSpecifier, 'index.tsx'),
path.resolve(dirPath, moduleSpecifier, 'index.js'),
path.resolve(dirPath, moduleSpecifier, 'index.jsx')
];
for (const potentialPath of potentialPaths) {
const file = project.getSourceFile(potentialPath);
if (file) return file;
}
}
// For absolute imports based on tsconfig paths or node_modules
return project.getSourceFile(sf =>
sf.getFilePath().includes(moduleSpecifier.replace(/^[~@]/, ''))
);
} catch (e) {
console.error(`Error resolving module: ${moduleSpecifier}`, e);
return undefined;
}
}
// Improved tree printing - now with relative paths and smart traversal
function printDependencyTree(
rootPath: string,
importMap: Map<string, Set<string>>,
traversedModules: Set<string>,
depth: number = 0,
currentPath: Set<string> = new Set()
): void {
const indent = ' '.repeat(depth);
const relativePath = getRelativePath(rootPath);
// Print current node
console.log(`${indent}* ${relativePath}`);
// Check for circular dependency
if (currentPath.has(rootPath)) {
console.log(`${indent} * (CIRCULAR REFERENCE)`);
return;
}
// Check if we've already traversed this module
if (traversedModules.has(rootPath)) {
console.log(`${indent} * (MODULE ALREADY TRAVERSED, SKIPPING)`);
return;
}
// Mark as traversed
traversedModules.add(rootPath);
// Add current path to the path we're exploring
currentPath.add(rootPath);
// Get and sort dependencies
const dependencies = importMap.get(rootPath) || new Set<string>();
const sortedDeps = Array.from(dependencies).sort((a, b) =>
getRelativePath(a).localeCompare(getRelativePath(b))
);
// Print each dependency
for (const dependency of sortedDeps) {
printDependencyTree(
dependency,
importMap,
traversedModules,
depth + 1,
new Set(currentPath)
);
}
}
}
const result = traceAllImports(entryFilePath, tsConfigPath);
console.log(`Direct dependencies of \`${entryFilePath}\`:`, result.directDependencies);
console.log('All modules loaded:', result.allLoadedFiles.length);
console.log('\nDependency Tree:');
result.printTree();
Direct dependencies of `/home/[CENSORED]/src/ui/components/AppPage/useAppPage.ts`: [ 'src/ui/components/AppPage/AppPageContext.ts' ]
All modules loaded: 3211
Dependency Tree:
* src/ui/components/AppPage/useAppPage.ts
* src/ui/components/AppPage/AppPageContext.ts
* src/ui/containers/AsideNavigation/AsideNavigation.tsx
* src/ui/containers/AsideNavigation/AsideNavigationActions.ts
* src/ui/containers/AsideNavigation/index.ts
* src/ui/containers/AsideNavigation/AsideNavigation.tsx
* (CIRCULAR REFERENCE)
* src/ui/containers/AsideNavigation/AsideNavigationTypes.ts
* src/ui/store/reducers/settings/selectors.ts
* src/ui/services/settings.ts
* src/server/components/html/types.ts
* src/server/types/core.ts
* src/server/types/env.ts
* src/server/types/middlewares.ts
* src/server/middlewares/internal-guard.ts
* src/server/components/util.ts
* src/server/components/api/index.ts
* packages/monitoring-logs/src/LogsProvider/logsProviderUtils/httpService.ts
* packages/monitoring-logs/src/api/grpc/types.ts
* packages/monitoring-logs/src/LogsTypes.ts
* packages/monitoring-logs/src/api/grpc/index.ts
* packages/monitoring-logs/src/api/grpc/actions.ts
* packages/monitoring-logs/src/api/grpc/common.ts
* packages/monitoring-logs/src/LogsProvider/logsProviderUtils/httpService.ts
* (CIRCULAR REFERENCE)
* packages/monitoring-logs/src/api/grpc/types.ts
* (CIRCULAR REFERENCE)
* packages/monitoring-logs/src/utils/index.ts
* packages/monitoring-logs/src/utils/cn.ts
* packages/monitoring-logs/src/utils/createMonitoringLogsUrl.ts
* packages/monitoring-logs/src/LogsTypes.ts
* (CIRCULAR REFERENCE)
* packages/monitoring-logs/src/utils/logEntryToLogsTableItem.ts
* packages/monitoring-logs/src/api/index.ts
* packages/monitoring-logs/src/api/grpc/index.ts
* (CIRCULAR REFERENCE)
* packages/monitoring-logs/src/utils/timeseries.ts
* packages/monitoring-logs/src/api/index.ts
* (CIRCULAR REFERENCE)
* packages/monitoring-logs/src/LogsTypes.ts
* (CIRCULAR REFERENCE)
* packages/monitoring-logs/src/utils/operator.ts
* packages/monitoring-logs/src/utils/protobuf.ts
* packages/monitoring-logs/src/utils/safeParseJson.ts
* packages/monitoring-logs/src/utils/sort.ts
* packages/monitoring-logs/src/utils/timeseries.ts
* (MODULE ALREADY TRAVERSED, SKIPPING)
* packages/monitoring-logs/src/api/grpc/common.ts
* (MODULE ALREADY TRAVERSED, SKIPPING)
* packages/monitoring-logs/src/api/grpc/endpoints.ts
* packages/monitoring-logs/src/api/grpc/sdkTypes.ts
* packages/monitoring-logs/src/api/grpc/actions.ts
* (MODULE ALREADY TRAVERSED, SKIPPING)
* packages/monitoring-logs/src/api/grpc/endpoints.ts
* (MODULE ALREADY TRAVERSED, SKIPPING)
* packages/monitoring-logs/src/api/grpc/types.ts
* (CIRCULAR REFERENCE)
* packages/monitoring-logs/src/components/LogsEditor/LogsMonacoProvider/suggests-visitor/types.ts
* packages/monitoring-logs/src/index.ts
* packages/monitoring-logs/src/components/index.ts
...
(truncated, actual output is 5.3 MiB large)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment