Skip to content

Commit

Permalink
some code for dependency analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
seveibar committed Dec 15, 2024
1 parent 15bb8c3 commit f8d2cb3
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 0 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,8 @@ npm install -g @tscircuit/snippets-cli

When you run `snippets dev`, we start a local
server that uses the [@tscircuit/file-server](https://github.com/tscircuit/file-server) and [@tscircuit/runframe](https://github.com/tscircuit/runframe) (on the browser)

We use commanderjs to define the CLI commands inside
of `cli/main.ts`

Utility functions are defined in `lib/*`
127 changes: 127 additions & 0 deletions lib/dependency-analysis/DependencyAnalyzer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import * as ts from 'typescript';
import * as path from 'path';
import * as fs from 'fs';

interface DependencyAnalyzerOptions {
baseDir: string;
includeNodeModules?: boolean;
includeCssModules?: boolean;
}

class DependencyAnalyzer {
private readonly baseDir: string;
private readonly includeNodeModules: boolean;
private readonly includeCssModules: boolean;
private readonly cache: Map<string, Set<string>> = new Map();

constructor(options: DependencyAnalyzerOptions) {
this.baseDir = options.baseDir;
this.includeNodeModules = options.includeNodeModules ?? false;
this.includeCssModules = options.includeCssModules ?? true;
}

public analyze(filePath: string): Set<string> {
if (this.cache.has(filePath)) {
return this.cache.get(filePath)!;
}

const dependencies = new Set<string>();
const sourceFile = this.createSourceFile(filePath);

if (!sourceFile) {
return dependencies;
}

this.visitNode(sourceFile, dependencies);
this.cache.set(filePath, dependencies);

return dependencies;
}

private createSourceFile(filePath: string): ts.SourceFile | null {
try {
const content = fs.readFileSync(filePath, 'utf-8');
return ts.createSourceFile(
filePath,
content,
ts.ScriptTarget.Latest,
true
);
} catch (error) {
console.error(`Error reading file ${filePath}:`, error);
return null;
}
}

private visitNode(node: ts.Node, dependencies: Set<string>): void {
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
const moduleSpecifier = node.moduleSpecifier;
if (moduleSpecifier && ts.isStringLiteral(moduleSpecifier)) {
const importPath = moduleSpecifier.text;
this.addDependency(importPath, dependencies);
}
}

// Check for dynamic imports
if (ts.isCallExpression(node) &&
node.expression.kind === ts.SyntaxKind.ImportKeyword) {
const argument = node.arguments[0];
if (argument && ts.isStringLiteral(argument)) {
this.addDependency(argument.text, dependencies);
}
}

ts.forEachChild(node, child => this.visitNode(child, dependencies));
}

private addDependency(importPath: string, dependencies: Set<string>): void {
// Skip node_modules unless explicitly included
if (!this.includeNodeModules && importPath.startsWith('node_modules')) {
return;
}

// Handle CSS modules if enabled
if (this.includeCssModules && importPath.endsWith('.css')) {
const resolvedPath = this.resolvePath(importPath);
if (resolvedPath) {
dependencies.add(resolvedPath);
}
return;
}

// Resolve relative imports
if (importPath.startsWith('.')) {
const resolvedPath = this.resolvePath(importPath);
if (resolvedPath) {
dependencies.add(resolvedPath);
}
}
}

private resolvePath(importPath: string): string | null {
try {
const extensions = ['.tsx', '.ts', '.jsx', '.js', '.css'];
const resolvedPath = path.resolve(this.baseDir, importPath);

// Try exact path first
if (fs.existsSync(resolvedPath)) {
return resolvedPath;
}

// Try with extensions
for (const ext of extensions) {
const pathWithExt = resolvedPath + ext;
if (fs.existsSync(pathWithExt)) {
return pathWithExt;
}
}

return null;
} catch (error) {
console.error(`Error resolving path ${importPath}:`, error);
return null;
}
}
}

export { DependencyAnalyzer, type DependencyAnalyzerOptions };
87 changes: 87 additions & 0 deletions lib/dependency-analysis/getLocalFileDependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import * as ts from 'typescript';
import * as path from 'path';
import * as fs from 'fs';

function getLocalFileDependencies(pathToTsxFile: string): string[] {
// Ensure absolute path
const absolutePath = path.resolve(pathToTsxFile);
const baseDir = path.dirname(absolutePath);

// Read and parse the file
const content = fs.readFileSync(absolutePath, 'utf-8');
const sourceFile = ts.createSourceFile(
absolutePath,
content,
ts.ScriptTarget.Latest,
true
);

const dependencies = new Set<string>();

// Recursively visit nodes to find imports
function visit(node: ts.Node) {
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
const moduleSpecifier = node.moduleSpecifier;
if (moduleSpecifier && ts.isStringLiteral(moduleSpecifier)) {
const importPath = moduleSpecifier.text;
// Only process local imports (starting with . or ..)
if (importPath.startsWith('.')) {
resolveAndAddDependency(importPath);
}
}
}

// Handle dynamic imports
if (ts.isCallExpression(node) &&
node.expression.kind === ts.SyntaxKind.ImportKeyword) {
const argument = node.arguments[0];
if (argument && ts.isStringLiteral(argument)) {
const importPath = argument.text;
if (importPath.startsWith('.')) {
resolveAndAddDependency(importPath);
}
}
}

ts.forEachChild(node, visit);
}

// Helper to resolve and add dependency paths
function resolveAndAddDependency(importPath: string) {
const extensions = ['.tsx', '.ts', '.jsx', '.js', '.css', '.scss', '.sass', '.less'];
let resolvedPath = path.resolve(baseDir, importPath);

// Check if path exists as-is
if (fs.existsSync(resolvedPath)) {
dependencies.add(resolvedPath);
return;
}

// Try with extensions
for (const ext of extensions) {
const pathWithExt = resolvedPath + ext;
if (fs.existsSync(pathWithExt)) {
dependencies.add(pathWithExt);
return;
}
}

// Check for index files in directories
if (fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isDirectory()) {
for (const ext of extensions) {
const indexPath = path.join(resolvedPath, `index${ext}`);
if (fs.existsSync(indexPath)) {
dependencies.add(indexPath);
return;
}
}
}
}

// Start the traversal
visit(sourceFile);

return Array.from(dependencies);
}

export { getLocalFileDependencies };
4 changes: 4 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
"baseUrl": ".",
"paths": {
"lib/*": ["lib/*"]
},

// Bundler mode
"moduleResolution": "bundler",
Expand Down

0 comments on commit f8d2cb3

Please sign in to comment.