11import * as fs from 'fs/promises' ;
22import * as path from 'path' ;
33import type { AnalyzeOptions , AnalyzeResult , PackageAnalysis } from './types.js' ;
4+ import createDebug from 'debug' ;
5+
6+ const debug = createDebug ( 'depoptimize:analyzer' ) ;
47
58export class NodeModulesAnalyzer {
69 constructor ( private options : AnalyzeOptions = { } ) { }
710
811 async analyze ( projectPath : string = process . cwd ( ) ) : Promise < AnalyzeResult > {
12+ debug ( 'Starting analysis for project: %s' , projectPath ) ;
913 const nodeModulesPath = path . join ( projectPath , 'node_modules' ) ;
1014 const sizeThreshold = ( this . options . sizeThreshold || 10 ) * 1024 * 1024 ; // Convert MB to bytes
1115 const depthThreshold = this . options . depthThreshold || 5 ;
16+ debug ( 'Configuration - sizeThreshold: %d bytes, depthThreshold: %d' , sizeThreshold , depthThreshold ) ;
1217
1318 const result : AnalyzeResult = {
1419 totalPackages : 0 ,
@@ -22,43 +27,53 @@ export class NodeModulesAnalyzer {
2227 // Check if node_modules exists
2328 const nodeModulesExists = await this . directoryExists ( nodeModulesPath ) ;
2429 if ( ! nodeModulesExists ) {
30+ debug ( 'node_modules not found at: %s' , nodeModulesPath ) ;
2531 return result ;
2632 }
33+ debug ( 'Found node_modules at: %s' , nodeModulesPath ) ;
2734
2835 // Get all packages in node_modules
2936 const packages = await this . getAllPackages ( nodeModulesPath ) ;
3037 result . totalPackages = packages . length ;
38+ debug ( 'Found %d packages in node_modules' , packages . length ) ;
3139
3240 // Analyze each package
3341 for ( const pkg of packages ) {
42+ debug ( 'Analyzing package: %s' , pkg . name ) ;
3443 const analysis = await this . analyzePackage ( pkg . path , pkg . name ) ;
3544
3645 result . totalSize += analysis . size ;
3746
3847 // Check if package exceeds size threshold
3948 if ( analysis . size > sizeThreshold ) {
49+ debug ( 'Large package detected: %s (%d bytes)' , pkg . name , analysis . size ) ;
4050 result . largePackages . push ( analysis ) ;
4151 }
4252
4353 // Check if package exceeds depth threshold
4454 if ( analysis . depth > depthThreshold ) {
55+ debug ( 'Deep package detected: %s (depth: %d)' , pkg . name , analysis . depth ) ;
4556 result . deepPackages . push ( analysis ) ;
4657 }
4758 }
4859
4960 // Sort results by size/depth descending
5061 result . largePackages . sort ( ( a , b ) => b . size - a . size ) ;
5162 result . deepPackages . sort ( ( a , b ) => b . depth - a . depth ) ;
63+ debug ( 'Analysis complete - total size: %d bytes, large packages: %d, deep packages: %d' ,
64+ result . totalSize , result . largePackages . length , result . deepPackages . length ) ;
5265
5366 } catch ( error ) {
5467 // Handle errors gracefully
68+ debug ( 'Error during analysis: %O' , error ) ;
5569 console . warn ( `Warning: Failed to analyze node_modules: ${ error } ` ) ;
5670 }
5771
5872 return result ;
5973 }
6074
6175 private async getAllPackages ( nodeModulesPath : string ) : Promise < { name : string ; path : string } [ ] > {
76+ debug ( 'Scanning for packages in: %s' , nodeModulesPath ) ;
6277 const packages : { name : string ; path : string } [ ] = [ ] ;
6378
6479 try {
@@ -70,6 +85,7 @@ export class NodeModulesAnalyzer {
7085
7186 if ( entry . name . startsWith ( '@' ) ) {
7287 // Scoped packages
88+ debug ( 'Processing scoped package directory: %s' , entry . name ) ;
7389 try {
7490 const scopedEntries = await fs . readdir ( entryPath , { withFileTypes : true } ) ;
7591 for ( const scopedEntry of scopedEntries ) {
@@ -79,25 +95,30 @@ export class NodeModulesAnalyzer {
7995
8096 // Verify it's a valid package
8197 if ( await this . isValidPackage ( scopedPath ) ) {
98+ debug ( 'Found scoped package: %s' , packageName ) ;
8299 packages . push ( { name : packageName , path : scopedPath } ) ;
83100 }
84101 }
85102 }
86- } catch {
103+ } catch ( error ) {
87104 // Skip if can't read scoped directory
105+ debug ( 'Failed to read scoped directory %s: %O' , entry . name , error ) ;
88106 }
89107 } else {
90108 // Regular packages
91109 if ( await this . isValidPackage ( entryPath ) ) {
110+ debug ( 'Found package: %s' , entry . name ) ;
92111 packages . push ( { name : entry . name , path : entryPath } ) ;
93112 }
94113 }
95114 }
96115 }
97- } catch {
116+ } catch ( error ) {
98117 // Return empty array if can't read node_modules
118+ debug ( 'Failed to read node_modules directory: %O' , error ) ;
99119 }
100120
121+ debug ( 'Total packages found: %d' , packages . length ) ;
101122 return packages ;
102123 }
103124
@@ -112,6 +133,7 @@ export class NodeModulesAnalyzer {
112133 }
113134
114135 private async analyzePackage ( packagePath : string , packageName : string ) : Promise < PackageAnalysis > {
136+ debug ( 'Analyzing package details for: %s' , packageName ) ;
115137 const analysis : PackageAnalysis = {
116138 name : packageName ,
117139 size : 0 ,
@@ -122,18 +144,22 @@ export class NodeModulesAnalyzer {
122144 try {
123145 // Calculate package size
124146 analysis . size = await this . getDirectorySize ( packagePath ) ;
147+ debug ( 'Package %s size: %d bytes' , packageName , analysis . size ) ;
125148
126149 // Calculate dependency depth
127150 analysis . depth = await this . getDependencyDepth ( packagePath ) ;
151+ debug ( 'Package %s depth: %d' , packageName , analysis . depth ) ;
128152
129- } catch {
153+ } catch ( error ) {
130154 // If we can't analyze, just return default values
155+ debug ( 'Failed to analyze package %s: %O' , packageName , error ) ;
131156 }
132157
133158 return analysis ;
134159 }
135160
136161 private async getDirectorySize ( dirPath : string ) : Promise < number > {
162+ debug ( 'Calculating size for directory: %s' , dirPath ) ;
137163 let size = 0 ;
138164
139165 try {
@@ -145,31 +171,37 @@ export class NodeModulesAnalyzer {
145171 if ( entry . isDirectory ( ) ) {
146172 // Skip nested node_modules to avoid double counting
147173 if ( entry . name === 'node_modules' ) {
174+ debug ( 'Skipping nested node_modules in: %s' , dirPath ) ;
148175 continue ;
149176 }
150177 size += await this . getDirectorySize ( entryPath ) ;
151178 } else if ( entry . isFile ( ) ) {
152179 try {
153180 const stats = await fs . stat ( entryPath ) ;
154181 size += stats . size ;
155- } catch {
182+ } catch ( error ) {
156183 // Skip files we can't read
184+ debug ( 'Failed to read file %s: %O' , entryPath , error ) ;
157185 }
158186 }
159187 }
160- } catch {
188+ } catch ( error ) {
161189 // Return 0 if we can't read the directory
190+ debug ( 'Failed to read directory %s: %O' , dirPath , error ) ;
162191 }
163192
193+ debug ( 'Total size for %s: %d bytes' , dirPath , size ) ;
164194 return size ;
165195 }
166196
167197 private async getDependencyDepth ( packagePath : string , visited = new Set < string > ( ) ) : Promise < number > {
168198 // Prevent infinite recursion
169199 if ( visited . has ( packagePath ) ) {
200+ debug ( 'Circular dependency detected, skipping: %s' , packagePath ) ;
170201 return 0 ;
171202 }
172203 visited . add ( packagePath ) ;
204+ debug ( 'Calculating depth for: %s' , packagePath ) ;
173205
174206 try {
175207 const packageJsonPath = path . join ( packagePath , 'package.json' ) ;
@@ -182,14 +214,17 @@ export class NodeModulesAnalyzer {
182214 } ;
183215
184216 if ( ! dependencies || Object . keys ( dependencies ) . length === 0 ) {
217+ debug ( 'No dependencies found for: %s' , packagePath ) ;
185218 return 0 ;
186219 }
220+ debug ( 'Found %d dependencies for: %s' , Object . keys ( dependencies ) . length , packagePath ) ;
187221
188222 let maxDepth = 0 ;
189223 const nodeModulesPath = path . join ( packagePath , 'node_modules' ) ;
190224
191225 // Check if this package has its own node_modules
192226 if ( await this . directoryExists ( nodeModulesPath ) ) {
227+ debug ( 'Checking nested node_modules: %s' , nodeModulesPath ) ;
193228 for ( const depName of Object . keys ( dependencies ) ) {
194229 const depPath = await this . findDependencyPath ( nodeModulesPath , depName ) ;
195230 if ( depPath ) {
@@ -199,27 +234,33 @@ export class NodeModulesAnalyzer {
199234 }
200235 }
201236
237+ debug ( 'Max depth for %s: %d' , packagePath , maxDepth ) ;
202238 return maxDepth ;
203239
204- } catch {
240+ } catch ( error ) {
241+ debug ( 'Failed to calculate depth for %s: %O' , packagePath , error ) ;
205242 return 0 ;
206243 }
207244 }
208245
209246 private async findDependencyPath ( nodeModulesPath : string , depName : string ) : Promise < string | null > {
247+ debug ( 'Looking for dependency %s in %s' , depName , nodeModulesPath ) ;
210248 // Check for scoped package
211249 if ( depName . includes ( '/' ) ) {
212250 const depPath = path . join ( nodeModulesPath , depName ) ;
213251 if ( await this . directoryExists ( depPath ) ) {
252+ debug ( 'Found scoped dependency at: %s' , depPath ) ;
214253 return depPath ;
215254 }
216255 } else {
217256 const depPath = path . join ( nodeModulesPath , depName ) ;
218257 if ( await this . directoryExists ( depPath ) ) {
258+ debug ( 'Found dependency at: %s' , depPath ) ;
219259 return depPath ;
220260 }
221261 }
222262
263+ debug ( 'Dependency %s not found in %s' , depName , nodeModulesPath ) ;
223264 return null ;
224265 }
225266
0 commit comments