├── .babelrc ├── .gitignore ├── src ├── build-results │ ├── generateHtmlFiles.js │ ├── generateDependencyData.js │ └── generateTreeMapData.js ├── filterAndParse.js ├── flow.js ├── collectInputs.js ├── transform.js └── parser.js ├── README.md ├── LICENSE ├── visualization ├── index.html ├── dependencies.html ├── style.css └── highcharts-treemap.html └── package.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-react", "@babel/preset-flow", "@babel/preset-typescript", "@babel/preset-env"], 3 | "plugins": ["@babel/plugin-syntax-jsx"] 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files generated by CodeMapper 2 | CodeMapper 3 | 4 | # VSCode settings 5 | .vscode 6 | 7 | # Dependency directories 8 | node_modules/ 9 | 10 | # dotenv environment variables file 11 | .env 12 | -------------------------------------------------------------------------------- /src/build-results/generateHtmlFiles.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs').promises; 2 | const path = require('path'); 3 | 4 | // this creates all the html files so the end user can see the results of their codebase analysis 5 | const generateHTMLfiles = async (pathToSource, pathToDestination) => { 6 | const files = await fs.readdir(pathToSource); 7 | 8 | files.forEach(async (file) => { 9 | try { 10 | const data = await fs.readFile(path.resolve(pathToSource, file)); 11 | 12 | await fs.writeFile( 13 | path.resolve(pathToDestination, file), 14 | data, 15 | 'utf8', 16 | (err) => { 17 | if (err) throw err; 18 | } 19 | ); 20 | } catch (error) { 21 | console.log('Error while generating final html files: ', error); 22 | } 23 | }); 24 | }; 25 | 26 | module.exports = generateHTMLfiles; 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodeMapper 2 | 3 | This project will soon be an NPM package to make use simple and easy! 4 | 5 | For now, to use this repo as it is, download everything and run "npm start" in the command line. A small application will run that will ask for the root path to the project you'd like to analyze. This means the codebase you're analyzing needs to be available on your local machine! 6 | 7 | Once you point CodeMapper to the right place, you'll be able to select which files and folders at the root level of the project you'd like to include for analyzing. 8 | 9 | Once that's done, open up the index.html file in the newly generated CodeMapper/Visualization folder in your browser. You'll see a page with a two buttons leading to different visualizations with more details about your project. 10 | 11 | P.S. CodeMapper shines most when looking at JavaScript-heavy projects, as we're able to give more details about JavaScript files. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /visualization/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | CodeMapper - Project Tree 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |

CodeMapper

20 |

Which results would you like to go to?

21 |
22 | 23 | 24 |
25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /src/filterAndParse.js: -------------------------------------------------------------------------------- 1 | const { parser } = require('./parser'); 2 | 3 | // this filters the whole tree recursively to create an flat array of JS file objects 4 | // each time we come to the top of this function, we'll be looking at a new directory 5 | // it goes over one level of one directory at a time 6 | const filterAndParse = (folder, jsFileArray = []) => { 7 | // iterate over the array of objects 8 | for (let i = 0; i < folder.length; i += 1) { 9 | // save a reference to the current file or folder object 10 | const current = folder[i]; 11 | // if "isDirectory" is true for this object... 12 | // check to see if content is an array with at least one element 13 | if (current.isDirectory === true && current.content.length >= 1) { 14 | // if so, pass the folders contents into handleFolder recursively, along with the file array 15 | filterAndParse(current.content, jsFileArray); 16 | // if there are no contents we move on because it's an empty folder 17 | } else if (current.extension === '.js') { 18 | // if isDirectory was false, this is a file, so check if this object has extension ".js" 19 | // if it does, add the object to our jsFileArray 20 | jsFileArray.push(current); 21 | // and then we call parser because it's a JS file 22 | parser(current); 23 | } 24 | } 25 | }; 26 | 27 | // this allows this whole piece of functionality to be called from another place 28 | module.exports = { filterAndParse }; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codemapper", 3 | "version": "1.0.0", 4 | "description": "Analyzes a codebase to give results about the project, particularly the javascript files. You can get a visual or non-visual representation of the project at a high-level along with and the imports, exports, and function definitions and calls for all JS files.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node src/collectInputs.js", 8 | "generate": "webpack", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/oslabs-beta/CodeMapper.git" 14 | }, 15 | "author": "RabbitHole", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/oslabs-beta/CodeMapper/issues" 19 | }, 20 | "homepage": "https://github.com/oslabs-beta/CodeMapper#readme", 21 | "dependencies": { 22 | "@babel/core": "^7.11.0", 23 | "@babel/generator": "^7.11.0", 24 | "@babel/parser": "^7.11.0", 25 | "@babel/plugin-syntax-jsx": "^7.10.4", 26 | "@babel/preset-env": "^7.11.0", 27 | "@babel/preset-flow": "^7.10.4", 28 | "@babel/preset-react": "^7.10.4", 29 | "@babel/preset-typescript": "^7.10.4", 30 | "@babel/traverse": "^7.11.0", 31 | "chalk": "^4.1.0", 32 | "figlet": "^1.5.0", 33 | "fs": "0.0.1-security", 34 | "inquirer": "^7.3.3", 35 | "open": "^7.3.0", 36 | "path": "^0.12.7" 37 | }, 38 | "bin": { 39 | "codemapper": "./src/collectInputs.js" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/build-results/generateDependencyData.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | // this function accept the filetree and iterates over it 4 | // to generate import and export data as an array of arrays 5 | // where each inner array has the exporting file first, the importing file 6 | // second, and then the number 1 (necessary to represent the weight of the 7 | // line we want to draw between the two - for now we'll have them all be 1, 8 | // later it would be ideal to show how many things are being exported/imported) 9 | const generateDependencyData = (finalTree, importExportData, pathToDir) => { 10 | // iterate over the array in the final tree 11 | for (let i = 0; i < finalTree.length; i += 1) { 12 | // if the object has an imports property, iterate over the array in imports 13 | if (finalTree[i].imported && finalTree[i].imported.length > 0) { 14 | // for each item in imports, add to the import export array a new array with the name of the exporting file, 15 | // importing file, and the number 1 (required by the dependency wheel chart, represents the thickness of the line 16 | // connecting the two lines) 17 | for (let j = 0; j < finalTree[i].imported.length; j += 1) { 18 | importExportData.push([ 19 | finalTree[i].imported[j].fileName, 20 | finalTree[i].name, 21 | 1, 22 | ]); 23 | } 24 | } 25 | // if the object is a directory with content, recursively look over the contents 26 | if ( 27 | finalTree[i].isDirectory === true && 28 | finalTree[i].content && 29 | finalTree[i].content.length > 0 30 | ) { 31 | generateDependencyData(finalTree[i].content, importExportData, pathToDir); 32 | } 33 | } 34 | 35 | fs.writeFileSync( 36 | `${pathToDir}/CodeMapper/Data/dependencies.js`, 37 | `const dependencyData = ${JSON.stringify(importExportData, null, 2)}`, 38 | 'utf8', 39 | (err) => { 40 | if (err) throw err; 41 | } 42 | ); 43 | }; 44 | 45 | module.exports = { generateDependencyData }; 46 | -------------------------------------------------------------------------------- /src/build-results/generateTreeMapData.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-syntax */ 2 | /* eslint-disable guard-for-in */ 3 | const fs = require('fs'); 4 | 5 | const points = []; 6 | const colors = [ 7 | '#d1f510', 8 | '#856677', 9 | '#ae9ef0', 10 | '#0f687a', 11 | '#ceaf73', 12 | '#18425d', 13 | '#8f2392', 14 | '#d17715', 15 | '#a10475', 16 | '307fc9', 17 | '#53A4EF', 18 | '#21212D', 19 | '#90ED7D', 20 | '#D37D37', 21 | '#4F57EA', 22 | '#DD5676', 23 | '#F2D82E', 24 | '#117271', 25 | '#CE3D3D', 26 | '#65C6BE', 27 | ]; 28 | 29 | // this is where we build out all the data for the nested object representing all the files and folders 30 | // in particular we add in extra info about JS files - function calls, function definitions, imports, and exports 31 | const generateTreeMapData = (data, parentId) => { 32 | for (const item in data) { 33 | const id = `id_${item.toString()}`; 34 | 35 | const newPoint = { 36 | name: data[item].name, 37 | 38 | color: colors[Math.round(Math.random() * 20)], 39 | }; 40 | if (data[item].functionCalls) { 41 | newPoint.functionCalls = data[item].functionCalls; 42 | } 43 | if (data[item].imported) { 44 | newPoint.imported = data[item].imported; 45 | } 46 | if (data[item].functionDeclarations) { 47 | newPoint.functionDeclarations = data[item].functionDeclarations; 48 | } 49 | if (data[item].exported) { 50 | newPoint.exported = data[item].exported; 51 | } 52 | 53 | if (parentId) { 54 | newPoint.parent = `${parentId}`; 55 | newPoint.id = `${parentId}_${item.toString()}`; 56 | } else { 57 | newPoint.id = id; 58 | } 59 | 60 | points.push(newPoint); 61 | if (data[item].isDirectory) { 62 | generateTreeMapData(data[item].content, newPoint.id); 63 | } else { 64 | newPoint.value = Math.round(data[item].size / 100); 65 | } 66 | } 67 | return points; 68 | }; 69 | 70 | // add data series to treeMapData object 71 | async function writeTreeMapData(data, pathToDir) { 72 | const dataSeries = await generateTreeMapData(data); 73 | 74 | // write to the resulting tree object 75 | fs.writeFile( 76 | `${pathToDir}/CodeMapper/Data/treeMapData.js`, 77 | `const treeMapData = ${JSON.stringify(dataSeries, null, 2)}`, 78 | (err) => { 79 | if (err) throw err; 80 | } 81 | ); 82 | } 83 | 84 | module.exports = { writeTreeMapData }; 85 | -------------------------------------------------------------------------------- /src/flow.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const chalk = require('chalk'); 4 | const open = require('open'); 5 | const { filterAndParse } = require('./filterAndParse'); 6 | const { generateDependencyData } = require('./build-results/generateDependencyData'); 7 | const { writeTreeMapData } = require('./build-results/generateTreeMapData'); 8 | const generateHTMLfiles = require('./build-results/generateHtmlFiles'); 9 | 10 | async function flow(fileTree, pathToDir) { 11 | // make container folders for required CodeMapper files so we can put them in a person's project 12 | if (!fs.existsSync(`${pathToDir}/CodeMapper`)) { 13 | fs.mkdirSync(`${pathToDir}/CodeMapper`); 14 | } 15 | if (!fs.existsSync(`${pathToDir}/CodeMapper/Data`)) { 16 | fs.mkdirSync(`${pathToDir}/CodeMapper/Data`); 17 | } 18 | if (!fs.existsSync(`${pathToDir}/CodeMapper/Visualization`)) { 19 | fs.mkdirSync(`${pathToDir}/CodeMapper/Visualization`); 20 | } 21 | 22 | // generate html files for the Visualization directory 23 | await generateHTMLfiles( 24 | path.resolve(process.cwd(), 'visualization'), 25 | `${pathToDir}/CodeMapper/Visualization`, 26 | ); 27 | 28 | try { 29 | if (fileTree !== undefined) { 30 | // call filterAndParse on the fileTree to get an array of pointers to the objects 31 | // that represent the JS files in the project 32 | // this will also pass all the JS files to the parser 33 | filterAndParse(fileTree); 34 | } 35 | } catch (err) { 36 | // ** we need to show a user friendly error to the CLI here 37 | // but for now, this will do ** 38 | console.error(`\n\x1b[31mError in flow.js with filterAndParse(fileTree): ${err.message}\x1b[37m`); 39 | } 40 | 41 | try { 42 | // add the data about all the files to our file tree in the Data directory 43 | // since we filtered out the JS files and parsed them, this will include 44 | // extra details about any JS files 45 | fs.writeFile( 46 | `${pathToDir}/CodeMapper/Data/fileTree.js`, 47 | `const treeMapData = ${JSON.stringify(fileTree, null, 2)}`, 48 | (err) => { 49 | if (err) throw err; 50 | } 51 | ); 52 | // our original fileTree variable is also modified now to give us what we need for generating other results 53 | // so we're going to pass that into generateDependencyData so that we can convert it into the correct type 54 | // for treeMap chart, and into writeTreeMapData to give us the things we need for the TreeMap 55 | await Promise.all([ 56 | writeTreeMapData(fileTree, pathToDir), 57 | generateDependencyData(fileTree, [], pathToDir), 58 | ]); 59 | } catch (err) { 60 | console.error( 61 | // ** we need to show a user friendly error to the CLI here 62 | // but for now, this will do ** 63 | `\n\x1b[31mError in flow.js with creating visualisation data fileTree): ${err.message}\x1b[37m` 64 | ); 65 | } 66 | 67 | // once we've generated all the data and front-end files, we open the "home page" for the person in their browser 68 | (async () => { 69 | await open(`${pathToDir}/CodeMapper/Visualization/index.html`, { 70 | wait: false, 71 | }); 72 | })(); 73 | 74 | // ** also, spoiler, this only works half the time, so also let them know to do it manually just in case 75 | console.log( 76 | chalk.greenBright( 77 | `And we're done! To view the results, open the index.html file we've generated in the ${pathToDir}/CodeMapper/Visualization folder in any up-to-date browser.` 78 | ) 79 | ); 80 | } 81 | 82 | module.exports = flow; 83 | -------------------------------------------------------------------------------- /visualization/dependencies.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | CodeMapper - Project Dependencies 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |

CodeMapper - Project Dependencies

23 |
24 |
25 |
26 |
27 | 28 |

29 | To the left is a chart showing the dependencies between javascript 30 | files in this project. 31 |

32 |
33 |

34 | For each file on the outside ring, its exports are shown in the same 35 | color as the block right below the file name. 36 |

37 |
38 |

39 | Its imports are shown in another color - matching the color of the 40 | file that the imports are coming from. 41 |

42 |
43 |

44 | Files and node modules only appear here if they are importing or 45 | exporting something to a javascript file in the project, so there 46 | may be files that don't appear in this chart. 47 |

48 |
49 |

50 | If you hover over the line between any two files/modules, you'll get 51 | a box with the name of the exporting file/module first, then an 52 | arrow pointing right, and then the name of the file/module that's 53 | importing something from the first file. 54 |

55 | 56 |
57 |
58 |
59 | 60 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /visualization/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&display=swap'); 2 | 3 | * { 4 | box-sizing: border-box; 5 | margin: 0; 6 | padding: 0; 7 | font-family: 'Baloo Tamma 2', cursive; 8 | } 9 | 10 | html, 11 | body { 12 | width: 100%; 13 | height: 100%; 14 | } 15 | 16 | body { 17 | display: grid; 18 | place-items: center; 19 | background-color: #1e272e; 20 | /* background-color: #474848; */ 21 | font-family: 'IBM Plex Sans', sans-serif; 22 | font-size: 16px; 23 | } 24 | 25 | .container { 26 | display: flex; 27 | flex-direction: column; 28 | justify-content: flex-start; 29 | align-items: center; 30 | width: 100%; 31 | height: 100%; 32 | padding: 1rem 2.5rem 2.5rem; 33 | } 34 | 35 | h1 { 36 | color: #ffdd59; 37 | font-weight: 400; 38 | font-size: 3rem; 39 | margin: 0 0 2.5rem 0; 40 | letter-spacing: 0.2rem; 41 | } 42 | 43 | h2 { 44 | color: #3d3d3d; 45 | text-align: center; 46 | font-size: 1.7rem; 47 | margin: 0.4rem 0; 48 | text-decoration: underline; 49 | } 50 | 51 | h3 { 52 | color: #e0e0e0; 53 | font-size: 1.5rem; 54 | } 55 | 56 | p { 57 | line-height: 1.3rem; 58 | } 59 | 60 | button { 61 | padding: 7px 23px; 62 | background-color: #e0e0e0; 63 | font-size: 1rem; 64 | border-radius: 3px; 65 | border: none; 66 | margin: 10px; 67 | } 68 | 69 | #treemap-container { 70 | width: 80%; 71 | } 72 | 73 | #dependency-wheel-container { 74 | width: 80%; 75 | } 76 | 77 | .highcharts-data-table { 78 | background-color: #e0e0e0; 79 | } 80 | 81 | .highcharts-container { 82 | font-family: 'Helvetica', sans-serif; 83 | text-shadow: none; 84 | } 85 | 86 | .visual-container { 87 | display: flex; 88 | flex-direction: row; 89 | justify-content: space-evenly; 90 | width: 100%; 91 | height: 100%; 92 | align-items: stretch; 93 | } 94 | 95 | #data-vis { 96 | background-color: #1e272e; 97 | 98 | flex: 1 1 auto; 99 | } 100 | 101 | .side { 102 | background-color: #fff; 103 | padding: 1rem; 104 | position: relative; 105 | overflow: hidden; 106 | flex: 0 0 350px; 107 | } 108 | 109 | #details { 110 | position: absolute; 111 | top: 0; 112 | right: -200%; 113 | background: #303030; 114 | color: white; 115 | width: calc(100% + 2rem); 116 | height: 100%; 117 | overflow-y: scroll; 118 | /* pointer-events: none; */ 119 | transition: all 1s ease; 120 | } 121 | #details.opened { 122 | right: -2rem; 123 | transition: all 1s ease; 124 | } 125 | 126 | /* #details:after { 127 | position: absolute; 128 | display: block; 129 | content: '+'; 130 | width: 2rem; 131 | height: 2rem; 132 | right: 1.5rem; 133 | cursor: pointer; 134 | top: 0.5rem; 135 | right: 2rem; 136 | text-align: center; 137 | font-size: 2.5rem; 138 | font-weight: 600; 139 | line-height: 1.5rem; 140 | transform: rotate(45deg); 141 | pointer-events: all; 142 | transform-origin: 50% 50%; 143 | } 144 | #details:hover:after { 145 | transform: rotate(145deg); 146 | transition: transform 1s ease; 147 | transform-origin: 50% 50%; 148 | } */ 149 | #details h3 { 150 | line-height: 1.9; 151 | text-align: center; 152 | margin: 0.5rem 0 0 0; 153 | border-bottom: 2px #3a3c3c solid; 154 | text-transform: uppercase; 155 | } 156 | 157 | #details h4 { 158 | line-height: 240%; 159 | text-align: center; 160 | margin: 1rem 2rem 0 0; 161 | } 162 | /* #details h3:first-of-type { 163 | background: #4e7fa9; 164 | } */ 165 | /* .close { 166 | position: absolute; 167 | width: 2rem; 168 | height: 2rem; 169 | right: 0.9rem; 170 | z-index: 2; 171 | top: 0.5rem; 172 | } 173 | 174 | .close span { 175 | display: block; 176 | width: 100%; 177 | height: 100%; 178 | font-weight: 600; 179 | transform: rotate(45deg); 180 | 181 | font-size: 2rem; 182 | line-height: 1; 183 | color: #fff; 184 | } 185 | .close span:hover { 186 | transform: rotate(135deg); 187 | 188 | } */ 189 | ul { 190 | padding: 0 1rem 1rem 1rem; 191 | list-style: none; 192 | } 193 | li { 194 | position: relative; 195 | background: #3a3c3c; 196 | margin: 0.3rem 1.5rem 0.3rem 0.5rem; 197 | line-height: 1.2rem; 198 | font-size: 0.8rem; 199 | padding: 0 0.5rem; 200 | max-height: 2.5rem; 201 | overflow: hidden; 202 | } 203 | .yellow { 204 | color: #ffd635; 205 | font-weight: 600; 206 | } 207 | .yellow > span { 208 | color: white; 209 | font-weight: 400; 210 | } 211 | .pink { 212 | color: #ff03ff; 213 | font-weight: 600; 214 | } 215 | .pink > span { 216 | color: white; 217 | font-weight: 400; 218 | } 219 | .green { 220 | color: #54ff59; 221 | font-weight: 600; 222 | } 223 | .green > span { 224 | color: white; 225 | font-weight: 400; 226 | } 227 | .blue { 228 | color: #03a9f4; 229 | font-weight: 600; 230 | } 231 | .blue > span { 232 | color: white; 233 | font-weight:400; 234 | } 235 | canvas { 236 | cursor: pointer; 237 | } 238 | -------------------------------------------------------------------------------- /src/collectInputs.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable no-param-reassign */ 3 | 4 | const path = require('path'); 5 | const { readdir, stat } = require('fs').promises; 6 | const inquirer = require('inquirer'); 7 | const figlet = require('figlet'); 8 | const chalk = require('chalk'); 9 | const flow = require('./flow'); 10 | 11 | const cwd = process.cwd(); 12 | 13 | // some things we assume people will want to exclude from their codebase analysis 14 | const excludes = [ 15 | 'LICENSE', 16 | '.git', 17 | '.DS_Store', 18 | '.vscode', 19 | '.babelrc', 20 | 'package.json', 21 | 'package-lock.json', 22 | 'yarn.lock', 23 | '.md', 24 | '__MACOSX', 25 | '.gitignore', 26 | 'node_modules', 27 | 'README.md', 28 | ]; 29 | 30 | // ** this is the initial prompt which still needs some validation on the input 31 | const questions = [ 32 | { 33 | name: 'projectDirectory', 34 | type: 'input', 35 | message: 36 | "Add the full path of the codebase folder you'd like to analyze, or press enter to use the current directory:", 37 | default: cwd, 38 | } 39 | ]; 40 | 41 | const askQuestions = (question) => { 42 | return inquirer.prompt(question); 43 | }; 44 | 45 | // this shows our intro message in the CLI 46 | const init = () => { 47 | console.log(chalk.magenta('\n\n\n W E L C O M E to')); 48 | console.log( 49 | chalk.cyan( 50 | figlet.textSync('CodeMapper!', { 51 | // font: 'Ghost', 52 | horizontalLayout: 'default', 53 | verticalLayout: 'default', 54 | }) 55 | ) 56 | ); 57 | console.log( 58 | chalk.magenta( 59 | ' your favourite codebase analyser! \n\n' 60 | ) 61 | ); 62 | console.log( 63 | `How does it work? 64 | 1. Select the root folder for the codebase you'd like to analyze. 65 | 2. Select the files or folders at the top level of the project that you'd like to include in the analysis. 66 | 3. Blink and enjoy the result!` 67 | ); 68 | }; 69 | 70 | // this allows us to show the files and folders at the root level of the codebase 71 | const buildRootList = async (pathToDir) => { 72 | const list = []; 73 | const rootList = await readdir(pathToDir, { withFileTypes: true }); 74 | rootList.map((el) => { 75 | const temp = { 76 | name: `${el.name}`, 77 | checked: true, 78 | }; 79 | if (excludes.includes(el.name)) { 80 | temp.checked = false; 81 | } 82 | list.push(temp); 83 | }); 84 | 85 | questions.push({ 86 | type: 'checkbox', 87 | pageSize: list.length || 20, 88 | message: 89 | "Select files/folders to include, then press when you're done.", 90 | name: 'includes', 91 | choices: list, 92 | // showHelpTip: true, 93 | }); 94 | return rootList; 95 | }; 96 | 97 | const buildEntireList = async (dir, excluded, depth = 0) => { 98 | const subdirs = await readdir(dir); 99 | const files = await Promise.all( 100 | subdirs 101 | .filter((name) => { 102 | return excluded.indexOf(name) === -1; 103 | }) 104 | .map(async (subdir) => { 105 | const res = path.resolve(dir, subdir); 106 | const statistics = await stat(res); 107 | const item = { 108 | name: path.basename(res, path.extname(res)), 109 | path: path.dirname(res), 110 | fullname: res, 111 | extension: path.extname(res), 112 | depth, 113 | isDirectory: statistics.isDirectory(), 114 | size: statistics.size, 115 | }; 116 | if (statistics.isDirectory()) { 117 | depth += 1; 118 | item.content = []; 119 | const subArr = await buildEntireList(res, excluded, depth); 120 | item.content.push(...subArr); 121 | } else { 122 | item.name = path.basename(res); 123 | } 124 | return item; 125 | }) 126 | ); 127 | return files; 128 | }; 129 | 130 | const collectData = async () => { 131 | // show script introduction 132 | init(); 133 | 134 | // ask question 1 135 | const answers = await askQuestions(questions[0]); 136 | 137 | // get the project directory from user input 138 | const { projectDirectory } = answers; 139 | 140 | // get all files from the root level 141 | const rootListFiles = await buildRootList(projectDirectory); 142 | 143 | // prompt q2-> precize files that shell be inculed/excluded 144 | const includes = await askQuestions(questions[questions.length - 1]); 145 | 146 | // modify the answers object with excluded and included files of folders 147 | answers.included = includes.includes; 148 | 149 | answers.excluded = rootListFiles 150 | .map((el) => el.name) 151 | .filter((el) => answers.included.indexOf(el) === -1); 152 | 153 | console.log(chalk.magenta("Ok! We're currently analyzing your codebase.")); 154 | 155 | const entireList = await buildEntireList(projectDirectory, answers.excluded); 156 | 157 | flow(entireList, projectDirectory); 158 | }; 159 | 160 | collectData(); 161 | -------------------------------------------------------------------------------- /src/transform.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | // this file transforms the data from the babel traversing a file into 3 | // a usable structure by giving the visitor useful methods for traversal 4 | 5 | const generate = require('@babel/generator').default; 6 | 7 | const transform = {}; 8 | 9 | // turns function definitions into the data we need and adds it to the file tree 10 | transform.functionDefinition = ( 11 | fileObject, 12 | name, 13 | params, 14 | async, 15 | type, 16 | method, 17 | definition 18 | ) => { 19 | // create the object we want to add to the filetree for this function 20 | const functionInfo = {}; 21 | 22 | // check for the name 23 | if (name) { 24 | // if it exists, save it. Otherwise save name as 'anonymous' 25 | functionInfo.name = name; 26 | } else { 27 | functionInfo.name = 'anonymous'; 28 | } 29 | 30 | // add whether it's async or not 31 | functionInfo.async = async; 32 | 33 | // add type - function declaration, arrow function, or function expression 34 | functionInfo.type = type; 35 | 36 | // add whether it's a class method or not 37 | functionInfo.method = method; 38 | 39 | // check for the parameters and add them to an array 40 | if (params.length) { 41 | functionInfo.parameters = []; 42 | for (let i = 0; i < params.length; i += 1) { 43 | // this is for simple parameter names 44 | if (params[i].name) { 45 | functionInfo.parameters.push(params[i].name); 46 | } else if (params[i].left.name) { 47 | // this is for parameters that have a default assignment 48 | functionInfo.parameters.push( 49 | `${params[i].left.name} = ${JSON.stringify(params[i].right.elements)}` 50 | ); 51 | } else { 52 | functionInfo.parameters.push(JSON.stringify(generate(params[i]).code)); 53 | } 54 | } 55 | } 56 | 57 | // add function definition 58 | if (definition) { 59 | try { 60 | functionInfo.definition = definition; 61 | } catch (error) { 62 | console.log( 63 | `Catch statement - error adding definition for ${functionInfo.name}` 64 | ); 65 | } 66 | } else { 67 | console.log( 68 | `Else statement - error adding definition for ${functionInfo.name}` 69 | ); 70 | } 71 | 72 | // check for any inner function calls 73 | // functionInfo.innerFunctionCalls: [ 74 | // { 75 | // 'name': 'fs.stat', 76 | // 'type': 'function', 'method', or 'anonymous method' 77 | // 'arguments': [ 78 | // 'file', 79 | // 'stringifyFunction' 80 | // ], 81 | // 'parent': { 82 | // 'name': 'nameOfThing', 83 | // } 84 | // 'leftSibling': 85 | // scope: 'something', 86 | // recursiveCall: true 87 | // } 88 | // ] 89 | 90 | // check for general function calls 91 | // check whether it's an import or was defined in the file 92 | // how it communicates with the environment - 93 | // what data it takes in 94 | // what data it returns 95 | // check whether it's a pre-built function 96 | 97 | // and then add it into the file tree 98 | if (fileObject.functionDeclarations) { 99 | fileObject.functionDeclarations.push(functionInfo); 100 | } else { 101 | fileObject.functionDeclarations = []; 102 | fileObject.functionDeclarations.push(functionInfo); 103 | } 104 | }; 105 | 106 | // helper function for logical expressions 107 | const handleLogicalExpressions = (parent, result = '') => { 108 | const { left } = parent; 109 | const { right } = parent; 110 | const { operator } = parent; 111 | 112 | // we always start by adding the right side to the beginning 113 | if (right) { 114 | if (right.value) { 115 | if (right.type === 'NumericLiteral') { 116 | if (result) { 117 | result = `${operator} ${right.value} ${result}`; 118 | } else { 119 | result = `${operator} ${right.value}`; 120 | } 121 | } 122 | if (right.type === 'StringLiteral') { 123 | if (result) { 124 | result = `${operator} '${right.value}' ${result}`; 125 | } else { 126 | result = `${operator} '${right.value}'`; 127 | } 128 | } 129 | } 130 | if (right.name) { 131 | if (result) { 132 | result = `${operator} ${right.name} ${result}`; 133 | } else { 134 | result = `${operator} ${right.name}`; 135 | } 136 | } 137 | if (right.operator) { 138 | if (right.type === 'UnaryExpression') { 139 | if (result) { 140 | result = `${operator} ${right.operator} ${right.argument.value}`; 141 | } 142 | } 143 | } 144 | } 145 | 146 | // termination case 147 | if (left.type === 'Identifier') { 148 | // add the name and then return the result 149 | result = `${left.name} ${result}`; 150 | return result; 151 | } 152 | 153 | if (left.type === 'NumericLiteral') { 154 | // add the name and then return the result 155 | result = `${left.value} ${result}`; 156 | return result; 157 | } 158 | 159 | if (left.type === 'StringLiteral') { 160 | // add the name and then return the result 161 | result = `'${left.value}' ${result}`; 162 | return result; 163 | } 164 | 165 | if (left.type === 'MemberExpression') { 166 | // put together the name and return the result 167 | result = `${left.object.name}.${left.property.name} ${result}`; 168 | return result; 169 | } 170 | 171 | // otherwise, recurse 172 | return handleLogicalExpressions(left, result); 173 | }; 174 | 175 | // turns function calls into the data we need and adds it to the file tree 176 | transform.functionCall = (fileObject, name, type, args) => { 177 | const functionInfo = {}; 178 | 179 | // check for the name 180 | if (name) { 181 | // if it exists, save it. Otherwise save name as 'anonymous' 182 | functionInfo.name = name; 183 | } else { 184 | functionInfo.name = 'anonymous'; 185 | } 186 | 187 | try { 188 | // add type - function, method, or anonymous method 189 | functionInfo.type = type; 190 | } catch (error) { 191 | console.log(`Ran into issues with adding the type to the function info. Type is ${type}. Error is ${error}`); 192 | } 193 | 194 | functionInfo.arguments = []; 195 | 196 | try { 197 | if (args.length) { 198 | for (let i = 0; i < args.length; i += 1) { 199 | const arg = args[i]; 200 | let label; 201 | let argObject; 202 | // number or function call or variable 203 | if (arg.value) { 204 | if (arg.type === 'StringLiteral') { 205 | label = `'${arg.value}'`; 206 | } else { 207 | label = arg.value; 208 | } 209 | } else if (arg.type === 'NullLiteral') { 210 | label = 'null'; 211 | } else if (arg.type === 'NumericLiteral') { 212 | label = arg.value; 213 | } else if (arg.type === 'TemplateLiteral') { 214 | label = JSON.stringify(generate(args[i]).code); 215 | } else if (arg.type === 'LogicalExpression') { 216 | label = handleLogicalExpressions(arg); 217 | } else if (arg.callee && arg.callee.name) { 218 | label = arg.callee.name; 219 | } else if (arg.name) { 220 | label = arg.name; 221 | } else if (arg.type === 'ArrowFunctionExpression') { 222 | const node = arg; 223 | let callbackName; 224 | if (node.id) { 225 | callbackName = node.id.name; 226 | } else { 227 | // this adds a whole object for the function definition 228 | callbackName = 'anonymousFunction'; 229 | const nodeParams = node.params || []; 230 | 231 | // check for the arguments and add them to an array 232 | const callbackParams = []; 233 | if (nodeParams.length) { 234 | for (let i = 0; i < nodeParams.length; i += 1) { 235 | // this is for simple parameter names 236 | if (nodeParams[i].name) { 237 | callbackParams.push(nodeParams[i].name); 238 | } else if (nodeParams[i].left.name) { 239 | // this is for parameters that have a default assignment 240 | callbackParams.push( 241 | `${nodeParams[i].left.name} = ${JSON.stringify( 242 | nodeParams[i].right.elements 243 | )}` 244 | ); 245 | } 246 | } 247 | } 248 | 249 | const { async } = node; 250 | const argType = node.type; 251 | const method = false; 252 | const definition = generate(node).code; 253 | argObject = { 254 | callbackName, 255 | callbackParams, 256 | async, 257 | type: argType, 258 | method, 259 | definition, 260 | }; 261 | } 262 | } else if (arg.type === 'LogicalExpression') { 263 | // call helper function 264 | label = handleLogicalExpressions(arg); 265 | } else if (arg.type === 'CallExpression') { 266 | if (arg.callee && arg.callee.name) { 267 | label = arg.callee.name; 268 | } else if (arg.callee.object && arg.callee.property) { 269 | if (args.arguments) { 270 | // grab the inner arguments 271 | const innerArgs = []; 272 | for (let j = 0; i < args.arguments.length; j += 1) { 273 | innerArgs.push(JSON.stringify(generate(innerArgs[j]).code)); 274 | } 275 | label = `${arg.callee.object.name}.${arg.callee.property.name}(${innerArgs.slice(1, -1)})`; 276 | } else { 277 | label = `${arg.callee.object.name}.${arg.callee.property.name}()`; 278 | } 279 | } 280 | } else if (arg.type === 'MemberExpression') { 281 | label = JSON.stringify(generate(args[i]).code); 282 | } 283 | functionInfo.arguments.push(label || argObject); 284 | } 285 | } 286 | } catch (err) { 287 | console.log(`Error while trying to save arguments into filetree. Error is ${err}`); 288 | } 289 | 290 | // and then add it into the file tree 291 | if (fileObject.functionCalls) { 292 | fileObject.functionCalls.push(functionInfo); 293 | } else { 294 | fileObject.functionCalls = []; 295 | fileObject.functionCalls.push(functionInfo); 296 | } 297 | 298 | // idea for other things that could be handy to add 299 | // { 300 | // 'name': 'fs.stat', 301 | // 'type': 'function', 'method', or 'anonymous method' 302 | // 'arguments': [ 303 | // 'file', 304 | // 'stringifyFunction' 305 | // ], 306 | // 'parent': { 307 | // 'name': 'nameOfThing', 308 | // } 309 | // 'siblings': '', 310 | // scope: 'something', 311 | // recursiveCall: true 312 | // } 313 | }; 314 | 315 | transform.import = ( 316 | fileObject, 317 | fileName, 318 | fileType, 319 | methodUsed, 320 | variableSet, 321 | ) => { 322 | const importInfo = {}; 323 | 324 | // fileType will either be node module or local module 325 | importInfo.fileType = fileType; 326 | 327 | // name of the file or module being imported 328 | importInfo.fileName = fileName; 329 | 330 | // methodUsed will either be require or import 331 | importInfo.methodUsed = methodUsed; 332 | 333 | // variableSet will always be an array of objects with a name and type property on each 334 | importInfo.namedImports = variableSet; 335 | 336 | // and then add it into the file tree 337 | if (!fileObject.imported) { 338 | fileObject.imported = []; 339 | } 340 | fileObject.imported.push(importInfo); 341 | 342 | // we could also check if it's being used? 343 | }; 344 | 345 | transform.export = (fileObject, originalName, exportName, value, type, exportSource) => { 346 | const exportInfo = {}; 347 | exportInfo.originalName = originalName; 348 | exportInfo.exportName = exportName; 349 | exportInfo.value = value; 350 | exportInfo.type = type; 351 | exportInfo.exportSource = exportSource; 352 | // and then add it into the file tree 353 | if (!fileObject.exports) { 354 | fileObject.exported = []; 355 | } 356 | fileObject.exported.push(exportInfo); 357 | }; 358 | 359 | module.exports = { transform }; 360 | -------------------------------------------------------------------------------- /visualization/highcharts-treemap.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | CodeMapper - Project Tree 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

CodeMapper - Project Tree

23 |
24 |
25 |
26 | 27 |
28 |

Instructions

29 |

To the left is a graphic showing the file and folder structure of your project.


30 |

You will start out with the root level of your project being displayed. 31 | Any folders found will be shown as a collection of boxes that with the folder name centered over boxes. 32 | Any files will be displayed with their title centered over a single box, and will include the appropriate file extension in their title.


33 |

The size of each box represents the size of its respective file or folder relative to all other files at the same level. 34 | This can offer a high-level view of what files are largest in the project.


35 |

You can back out of any folder you've clicked into by clicking the "< Back" button in the top right of the area displaying the visualization.


36 |

To navigate inside a folder, click the box with its name and the graphic will change to show only the relevant files and nested folders within that folder.


37 |

To learn more about a Javascript file, click the box with its name on it. 38 | In the right sidebar you're reading these instructions in, this text will be replaced with the information about that particular file. 39 | That text will be removed again if you click a new file to analyze.

40 |
41 |
42 | 43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 476 | 477 | 478 | -------------------------------------------------------------------------------- /src/parser.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | const { parse } = require('@babel/parser'); 3 | const traverse = require('@babel/traverse').default; 4 | const generate = require('@babel/generator').default; 5 | const fs = require('fs'); 6 | const { transform } = require('./transform'); 7 | 8 | // we can use this directly if we'd rather pass each file to the parser one at a time 9 | const fileParser = (fileObject, filePath) => { 10 | // this reads the file so we can parse it 11 | let readFile; 12 | try { 13 | readFile = fs.readFileSync(filePath).toString(); 14 | } catch (err) { 15 | console.log(`Found an error while reading a file to generate the official AST for it: ${err}. File is ${fileObject.name}`); 16 | } 17 | 18 | // this parses the file into an AST so we can traverse it 19 | let parsedFile; 20 | try { 21 | parsedFile = parse(readFile, { 22 | sourceType: 'module', 23 | plugins: ['jsx', 'typescript'], 24 | }); 25 | } catch (err) { 26 | console.log(`Error while creating ast for traversing. Error is ${err} and relevant file is ${fileObject.name}`); 27 | } 28 | 29 | // this allows us to traverse the AST 30 | const visitor = { 31 | // call transform in here to change the results from babel parsing the JS files 32 | // into what we need for visualization and other types of sharing results 33 | // and save that info into our fileTree 34 | 35 | // This is for regular function expressions (not anonymous/arrow functions) 36 | FunctionDeclaration({ node }) { 37 | try { 38 | let name = 'anonymous'; 39 | if (node.id) { 40 | name = node.id.name; 41 | } 42 | const { params } = node; 43 | const { async } = node; 44 | const { type } = node; 45 | const method = false; 46 | const definition = generate(node).code; 47 | transform.functionDefinition(fileObject, name, params, async, type, method, definition); 48 | } catch (err) { 49 | console.log(`Found an error while parsing a function declaration node: ${err}`); 50 | } 51 | }, 52 | 53 | VariableDeclaration({ node }) { 54 | // This handles the arrow functions 55 | try { 56 | if (node.declarations[0].init && node.declarations[0].init.type === 'ArrowFunctionExpression') { 57 | const { name } = node.declarations[0].id; 58 | const params = node.declarations[0].init.params || []; 59 | const { async } = node.declarations[0].init; 60 | const { type } = node.declarations[0].init; 61 | const method = false; 62 | const definition = generate(node).code; 63 | transform.functionDefinition(fileObject, name, params, async, type, method, definition); 64 | } 65 | } catch (err) { 66 | console.log(`Found an error while parsing an arrow function definition in a variable declaration node: ${err}`); 67 | } 68 | 69 | // This handles the require statements without any extra details added on to the require invocation 70 | // (like a '.default' or settings object) 71 | if (node.declarations && node.declarations[0].init) { 72 | // We'll handle imports assigned to variables here 73 | // for example var promise = import("module-name"); 74 | try { 75 | if (node.declarations[0].init.callee && node.declarations[0].init.callee.type === 'Import') { 76 | const variable = {}; 77 | const variableSet = []; 78 | variable.name = node.declarations[0].id.name; 79 | variable.type = 'local name'; 80 | variableSet.push(variable); 81 | const fileName = node.declarations[0].init.arguments[0].value; 82 | fileName.trim(); 83 | 84 | let fileType = ''; 85 | if (fileName.charAt(0) === '.') { 86 | fileType = 'local module'; 87 | } else { 88 | fileType = 'node module'; 89 | } 90 | 91 | const methodUsed = 'dynamic import'; 92 | 93 | transform.import(fileObject, fileName, fileType, methodUsed, variableSet); 94 | } 95 | } catch (err) { 96 | console.log(`Found an error while parsing an import statement in a variable declaration node: ${err}`); 97 | } 98 | 99 | try { 100 | if ((node.declarations[0].init.type === 'CallExpression' && node.declarations[0].init.callee.name === 'require') || (node.declarations[0].init.type === 'MemberExpression' && node.declarations[0].init.object.callee && node.declarations[0].init.object.callee.name === 'require')) { 101 | const variableSet = []; 102 | 103 | // when we're naming the default we're bringing in 104 | if (node.declarations[0].id.type === 'Identifier') { 105 | const variable = {}; 106 | variable.name = node.declarations[0].id.name; 107 | variable.type = 'local name'; 108 | variableSet.push(variable); 109 | } 110 | 111 | // for when we destructure the things we're importing 112 | // when we're destructuring a specific thing we're importing 113 | if (node.declarations[0].id.type === 'ObjectPattern' && node.declarations[0].id.properties.length) { 114 | for (let i = 0; i < node.declarations[0].id.properties.length; i += 1) { 115 | const variable = {}; 116 | variable.name = node.declarations[0].id.properties[i].value.name; 117 | variable.type = 'original name'; 118 | variableSet.push(variable); 119 | } 120 | } 121 | 122 | // we may need to adjust this later if it's possible to chain more than one thing after the require invocation 123 | let fileName; 124 | if (node.declarations[0].init.arguments) { 125 | fileName = node.declarations[0].init.arguments[0].value; 126 | } else { 127 | fileName = node.declarations[0].init.object.arguments[0].value; 128 | // console.log(`No arguments property on init. Init is ${JSON.stringify(node.declarations[0].init)}`); 129 | } 130 | fileName.trim(); 131 | 132 | let fileType = ''; 133 | if (fileName.charAt(0) === '.') { 134 | fileType = 'local module'; 135 | } else { 136 | fileType = 'node module'; 137 | } 138 | 139 | const methodUsed = 'require'; 140 | 141 | // console.log(`file name is ${fileName} and file type is ${fileType} and method used is ${methodUsed} and variable set looks like ${JSON.stringify(variableSet)}`); 142 | 143 | transform.import(fileObject, fileName, fileType, methodUsed, variableSet); 144 | } 145 | } catch (err) { 146 | console.log(`Found an error while trying to parse a require statement that's a call or member expression: ${err}`); 147 | } 148 | } 149 | }, 150 | 151 | // This handles class methods 152 | ExpressionStatement({ node }) { 153 | try { 154 | if (node.expression.right && node.expression.left) { 155 | let name; 156 | const { type } = node.expression.right; 157 | if ( 158 | type === 'ArrowFunctionExpression' || 159 | type === 'FunctionExpression' 160 | ) { 161 | if (node.expression.left.object && node.expression.left.property) { 162 | name = `${node.expression.left.object.name}.${node.expression.left.property.name}`; 163 | } else { 164 | name = 'anonymousMethod'; 165 | } 166 | const params = node.expression.right.params || []; 167 | const { async } = node.expression.right; 168 | const method = true; 169 | const definition = generate(node).code; 170 | transform.functionDefinition( 171 | fileObject, 172 | name, 173 | params, 174 | async, 175 | type, 176 | method, 177 | definition 178 | ); 179 | } 180 | } 181 | } catch (err) { 182 | console.log(`Found an error while parsing a class method: ${err}.`); 183 | } 184 | 185 | // this handles module exports 186 | try { 187 | if (node.expression.type === 'AssignmentExpression' && node.expression.left && node.expression.left.object) { 188 | if (node.expression.left.object && node.expression.left.object.name === 'module' && node.expression.left.property && node.expression.left.property.name === 'exports') { 189 | let originalName = 'anonymous'; 190 | let exportName = originalName; 191 | // in the case that we're creating an object to export things in 192 | if (node.expression.right.type === 'ObjectExpression') { 193 | for (let i = 0; i < node.expression.right.properties.length; i += 1) { 194 | const current = node.expression.right.properties[i]; 195 | originalName = current.value.name; 196 | exportName = current.key.name; 197 | const value = 'unknown'; 198 | const type = 'module export'; 199 | let exportSource = 'current file'; 200 | if (node.source) { 201 | exportSource = node.source.value; 202 | } 203 | // in this case we have to loop through the object to get all the exports 204 | transform.export(fileObject, originalName, exportName, value, type, exportSource); 205 | } 206 | } else { // in the case that we have a simple named export 207 | if (node.expression.right && node.expression.right.name) { 208 | originalName = node.expression.right.name; 209 | } 210 | exportName = originalName; 211 | const value = 'unknown'; 212 | const type = 'module export'; 213 | let exportSource = 'current file'; 214 | if (node.source) { 215 | exportSource = node.source.value; 216 | } 217 | transform.export(fileObject, originalName, exportName, value, type, exportSource); 218 | } 219 | } 220 | } 221 | } catch (err) { 222 | console.log(`Found an error while parsing a module export: ${err}`); 223 | } 224 | }, 225 | 226 | // This handles functions that are defined inside of iffys 227 | FunctionExpression(path) { 228 | const { node } = path; 229 | // ignore any that are assignment expressions because they are handled in the above Expression Statement block 230 | try { 231 | if (path.parent.type !== 'AssignmentExpression') { 232 | let name; 233 | if (node.id) { 234 | name = node.id.name; 235 | } else { 236 | name = 'anonymousFunction'; 237 | } 238 | const params = node.params || []; 239 | const { async } = node; 240 | const { type } = node; 241 | const method = false; 242 | const definition = generate(node).code; 243 | transform.functionDefinition( 244 | fileObject, 245 | name, 246 | params, 247 | async, 248 | type, 249 | method, 250 | definition 251 | ); 252 | } 253 | } catch (err) { 254 | console.log(`Found an error while parsing a function defined inside an iffy: ${err}`); 255 | } 256 | }, 257 | 258 | // for outer function calls 259 | CallExpression({ node }) { 260 | let name; 261 | let type; 262 | 263 | // for regular functions 264 | try { 265 | if (node.callee && node.callee.type !== 'Import') { 266 | if (node.callee && node.callee.name) { 267 | // handling all regular function calls except require statements here 268 | if (node.callee.name) { 269 | name = node.callee.name; 270 | type = 'function'; 271 | } 272 | } else if (node.callee.object && node.callee.object.name) { 273 | // for regular class methods 274 | name = `${node.callee.object.name}.${node.callee.property.name}`; 275 | type = 'method'; 276 | } else if ( 277 | node.callee.object && 278 | node.callee.object.object && 279 | node.callee.object.property && 280 | node.callee.object.object.name && 281 | node.callee.object.property.name 282 | ) { 283 | // for methods called on object properties 284 | name = `${node.callee.object.object.name}.${node.callee.object.property.name}.${node.callee.property.name}`; 285 | type = 'method'; 286 | } else if (node.callee.property && node.callee.property.name) { 287 | // and for class/prototype methods we can't identify the object of 288 | name = `anonymous.${node.callee.property.name}`; 289 | type = 'anonymous method'; 290 | } else if (node.callee && node.callee.id && node.callee.id.name) { 291 | name = node.callee.id.name; 292 | type = 'immediately invoked function expression'; 293 | } else { 294 | name = 'anonymous'; 295 | type = 'anonymous function'; 296 | } 297 | // grab the arguments (this will be an empty array if no arguments are there) 298 | // we should revisit this because we should be handling the node logic here instead 299 | // or actually... perhaps this is how we should be handling all the node visitor patterns 300 | // and letting transform do the parsing out of the details for each thing 301 | 302 | const args = node.arguments; 303 | 304 | // console.log('type in parser is ', type); 305 | transform.functionCall(fileObject, name, type, args); 306 | } 307 | } catch (err) { 308 | console.log(`Found an error while gettings details of a regular function/method call that is not an import statement: ${err}.`); 309 | } 310 | 311 | // or if object type is call expression, then it's in a chain of methods, so we won't have the object name but we still get the method name 312 | // arguments are always at path.node.arguments 313 | 314 | // check the parent to see if it is program.body (something like that) 315 | // if it is, then it's an outer function call so we can process it here 316 | // console.log(path.contexts[0].scope.block.body); 317 | 318 | // // define a function that can look inside a call expression for more call expressions 319 | // if (path.parent.type === 'MemberExpression') { 320 | // console.log(`path.parent.type is MemberExpression for the node: ${JSON.stringify(path.node)}`); 321 | // } 322 | }, 323 | 324 | // This handles import statements 325 | ImportDeclaration({ node }) { 326 | const variableSet = []; 327 | 328 | try { 329 | if (node.specifiers) { 330 | for (let i = 0; i < node.specifiers.length; i += 1) { 331 | const variable = {}; 332 | const current = node.specifiers[i]; 333 | variable.name = current.local.name; 334 | 335 | // for when we select specific things to bring in 336 | if (current.type === 'ImportSpecifier') { 337 | variable.type = 'original name'; 338 | variable.originalName = variable.name; 339 | } else if (current.type === 'ImportDefaultSpecifier') { 340 | // if we only import one variable and it's not destructured 341 | variable.type = 'local name'; 342 | } else if (current.type === 'ImportNamespaceSpecifier') { 343 | variable.type = 'local name'; 344 | variable.originalName = '*'; 345 | } 346 | 347 | // if we know the original name because we're using an alias, add it 348 | if (current.imported && current.imported.name) { 349 | variable.originalName = current.imported.name; 350 | } else if (!variable.originalName) { 351 | variable.originalName = 'unknown'; 352 | } 353 | 354 | variableSet.push(variable); 355 | } 356 | const fileName = node.source.value.trim(); 357 | let fileType; 358 | if (fileName.charAt(0) === '.') { 359 | fileType = 'local module'; 360 | } else { 361 | fileType = 'node module'; 362 | } 363 | const methodUsed = 'import'; 364 | 365 | transform.import( 366 | fileObject, 367 | fileName, 368 | fileType, 369 | methodUsed, 370 | variableSet, 371 | ); 372 | } 373 | } catch (err) { 374 | console.log(`Found an error while parsing an import declaration node: ${err}`); 375 | } 376 | }, 377 | 378 | ExportNamedDeclaration({ node }) { 379 | if (node.declaration) { 380 | // for function declarations being exported 381 | try { 382 | if (node.declaration.type === 'FunctionDeclaration') { 383 | const originalName = node.declaration.id.name; 384 | const exportName = originalName; 385 | const value = generate(node.declaration).code || 'unknown'; 386 | const type = 'named export'; 387 | let exportSource = 'current file'; 388 | if (node.source) { 389 | exportSource = node.source.value; 390 | } 391 | transform.export(fileObject, originalName, exportName, value, type, exportSource); 392 | } 393 | } catch (err) { 394 | console.log(`Found an error while parsing a function declaration export: ${err}`); 395 | } 396 | 397 | // for class declarations being exported 398 | try { 399 | if (node.declaration.type === 'ClassDeclaration') { 400 | const originalName = node.declaration.id.name; 401 | const exportName = originalName; 402 | const value = generate(node.declaration).code || 'unknown'; 403 | const type = 'named export'; 404 | let exportSource = 'current file'; 405 | if (node.source) { 406 | exportSource = node.source.value; 407 | } 408 | transform.export(fileObject, originalName, exportName, value, type, exportSource); 409 | } 410 | } catch (err) { 411 | console.log(`Found an error while parsing a class declaration export: ${err}`); 412 | } 413 | 414 | // for variable declarations being exported 415 | try { 416 | if (node.declaration.declarations) { 417 | for (let i = 0; i < node.declaration.declarations.length; i += 1) { 418 | const originalName = node.declaration.declarations[i].id.name; 419 | const exportName = originalName; 420 | // default value 421 | let value = 'unknown'; 422 | // if we can get it though, set it here 423 | if (node.declaration.declarations[i].init) { 424 | value = node.declaration.declarations[i].init.name; 425 | } 426 | const type = 'named export'; 427 | let exportSource = 'current file'; 428 | if (node.source) { 429 | exportSource = node.source.value; 430 | } 431 | transform.export(fileObject, originalName, exportName, value, type, exportSource); 432 | } 433 | } 434 | } catch (err) { 435 | console.log(`Found an error while parsing a variable declaration export: ${err}`); 436 | } 437 | } 438 | 439 | // for object exports with variables passed in to build the object 440 | try { 441 | if (node.specifiers) { 442 | for (let i = 0; i < node.specifiers.length; i += 1) { 443 | const originalName = node.specifiers[i].local.name; 444 | let exportName = originalName; 445 | let type = 'named export'; 446 | if (node.specifiers[i].exported.name === 'default') { 447 | type = 'default export'; 448 | } else if (node.specifiers[i].exported.name) { 449 | exportName = node.specifiers[i].exported.name; 450 | } 451 | const value = 'unknown'; 452 | let exportSource = 'current file'; 453 | if (node.source) { 454 | exportSource = node.source.value; 455 | } 456 | transform.export(fileObject, originalName, exportName, value, type, exportSource); 457 | } 458 | } 459 | } catch (err) { 460 | console.log(`Found an error while parsing an object export with variables passed in to build the object: ${err}`); 461 | } 462 | }, 463 | 464 | ExportDefaultDeclaration({ node }) { 465 | if (node.declaration) { 466 | 467 | // for simple default exports with a variable name 468 | try { 469 | if (node.declaration.type === 'Identifier') { 470 | const originalName = node.declaration.name; 471 | const exportName = originalName; 472 | const value = 'unknown'; 473 | const type = 'default export'; 474 | const exportSource = 'current file'; 475 | transform.export(fileObject, originalName, exportName, value, type, exportSource); 476 | } 477 | } catch (err) { 478 | console.log(`Found an error while parsing a default export with a variable name: ${err}`); 479 | } 480 | 481 | // for default exports that are function definitions 482 | try { 483 | if (node.declaration.type === 'FunctionDeclaration') { 484 | let originalName = 'anonymous'; 485 | if (node.declaration.id) { 486 | originalName = node.declaration.id.name; 487 | } 488 | const exportName = originalName; 489 | const value = generate(node.declaration).code || 'unknown'; 490 | const type = 'default export'; 491 | const exportSource = 'current file'; 492 | transform.export(fileObject, originalName, exportName, value, type, exportSource); 493 | } 494 | } catch (err) { 495 | console.log(`Found an error while parsing a function definition as a default export: ${err}`); 496 | } 497 | 498 | // for default exports that are class declarations 499 | try { 500 | if (node.declaration.type === 'ClassDeclaration') { 501 | const originalName = node.declaration.id.name || 'anonymous'; 502 | const exportName = originalName; 503 | const value = generate(node.declaration).code || 'unknown'; 504 | const type = 'default export'; 505 | const exportSource = 'current file'; 506 | transform.export(fileObject, originalName, exportName, value, type, exportSource); 507 | } 508 | } catch (err) { 509 | console.log(`Found an error while parsing a class declaration as a default export: ${err}`); 510 | } 511 | } else { 512 | console.log('found a default export we couldn\'t process'); 513 | } 514 | }, 515 | 516 | ExportAllDeclaration({ node }) { 517 | try { 518 | const originalName = '*'; 519 | const exportName = originalName; 520 | const value = 'unknown'; 521 | const type = 'export all'; 522 | let exportSource = 'current file'; 523 | if (node.source) { 524 | exportSource = node.source.value; 525 | } 526 | transform.export(fileObject, originalName, exportName, value, type, exportSource); 527 | } catch (err) { 528 | console.log(`Found an error while parsing an export all declaration node: ${err}`); 529 | } 530 | }, 531 | }; 532 | 533 | // this traverses the AST (parsedFile) and uses the visitor object to determine what to do 534 | // with each part of the AST. this data all gets added to the fileTree 535 | traverse(parsedFile, visitor); 536 | }; 537 | 538 | // this is just an easy wrapper for calling the fileParser 539 | const parser = (fileObject) => { 540 | const filePath = fileObject.fullname; 541 | fileParser(fileObject, filePath); 542 | }; 543 | 544 | module.exports = { parser }; 545 | --------------------------------------------------------------------------------