├── .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 |
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 |
--------------------------------------------------------------------------------