import { cruise , IReporterOutput } from "dependency-cruiser" ; import { IDependency , IModule } from "dependency-cruiser/types/cruise-result" ; import { parse } from "@babel/parser" ; import path from "path" ; import fs from "fs/promises" ; import query from "esquery" ; import markdownTable from "markdown-table" ; const findRouting = async ( filePath ) => { try { const fileContent = await fs . readFile ( filePath , "utf-8" ) ; const AST = parse ( fileContent , { sourceType : "module" , plugins : [ "jsx" , "typescript" ] } ) ; const search = ( method , AST ) => { const selector = `CallExpression:has(MemberExpression > Identifier[name="${ method }"])` ; const results = query ( AST , selector ) ; return results . flatMap ( ( node ) => { const pathValue = node . arguments [ 0 ] !== undefined && node . arguments [ 0 ] . type === "StringLiteral" && node . arguments [ 0 ] . value ; if ( ! pathValue ) { return [ ] ; } const middlewareArguments = method === "use" ? node . arguments ?. slice ( 1 ) ?? [ ] : node . arguments ?. slice ( 1 , node . arguments . length - 1 ) ?? [ ] ; const middlewares = middlewareArguments . map ( ( arg ) => { return fileContent . slice ( arg . start , arg . end ) ; } ) ; return [ { method , path : pathValue , middlewares , range : [ node . start , node . end ] as [ number , number ] , loc : node . loc as { start : { line : number ; column : number ; } ; end : { line : number ; column : number ; } ; } } ] ; } ) ; } ; const methods = [ "get" , "post" , "delete" , "put" , "use" ] as const ; return methods . flatMap ( ( method ) => { return search ( method , AST ) ; } ) ; } catch { return [ ] ; } } ; export async function analyzeDependency ( { outputFormat , rootDir , rootBaseUrl = "" , includeOnly , doNotFollow } ) { const ROOT_DIR = rootDir ; const hasImportExpress = ( dep ) => { return ( ( dep . dependencyTypes . includes ( "npm" ) || dep . dependencyTypes . includes ( "npm-dev" ) ) && dep . module === "express" ) ; } ; const underTheRoot = ( module ) => { return toAbsolute ( module . source ) . startsWith ( ROOT_DIR ) ; } ; const hasModuleImportExpress = ( module ) => { return module . dependencies . some ( ( dep ) => hasImportExpress ( dep ) ) ; } ; const toAbsolute = ( f ) => { return path . resolve ( process . cwd ( ) , f ) ; } ; const toRelative = ( f ) => { return path . relative ( ROOT_DIR , toAbsolute ( f ) ) ; } ; const ARRAY_OF_FILES_AND_DIRS_TO_CRUISE = [ ROOT_DIR ] ; const cruiseResult = cruise ( ARRAY_OF_FILES_AND_DIRS_TO_CRUISE , { includeOnly , doNotFollow } ) ; if ( typeof cruiseResult . output !== "object" ) { throw new Error ( "NO OUTPUT" ) ; } const modules = cruiseResult . output . modules . filter ( hasModuleImportExpress ) . filter ( underTheRoot ) ; const allResults = await Promise . all ( modules . map ( async ( mo ) => { return { filePath : toAbsolute ( mo . source ) , routers : await findRouting ( toAbsolute ( mo . source ) ) } ; } ) ) ; if ( outputFormat === "markdown" ) { const table = [ [ "File" , "Method" , "Routing" , "Middlewares" , "FilePath" ] ] ; for ( const result of allResults ) { table . push ( [ `${ rootBaseUrl }${ toRelative ( result . filePath ) }` ] ) ; result . routers . forEach ( ( router ) => { table . push ( [ "" , router . method , router . path , router . middlewares . join ( ", " ) . split ( /\r?\n/g ) . join ( " " ) , `${ rootBaseUrl }${ toRelative ( result . filePath ) }#L${ router . loc . start . line }-${ router . loc . end . line }` ] ) ; } ) ; } return markdownTable ( table ) ; } else { return allResults ; } }