From 4337a6f1c0b06b14fcdca87da946c7f919137d1d Mon Sep 17 00:00:00 2001 From: Katalogo262f Date: Sat, 21 Sep 2024 18:42:51 +0530 Subject: [PATCH 1/2] nextjs file structure & duplicate child handled --- src/parser.ts | 380 +++++++++++++++++++++++++---------------- src/types/ImportObj.ts | 2 +- 2 files changed, 238 insertions(+), 144 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index 9b4c6fcf1..9bc233fb3 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,48 +1,63 @@ -import * as babelParser from '@babel/parser'; -import * as path from 'path'; -import * as fs from 'fs'; -import { getNonce } from './getNonce'; -import { Tree } from './types/Tree'; -import { ImportObj } from './types/ImportObj'; -import { File } from '@babel/types'; +//if multiple children of a parent have the same fileName/pathName then only keep one of them, like a set(). and if children have the same fileName/pathName but different parent then +import * as babelParser from "@babel/parser"; +import * as path from "path"; +import * as fs from "fs"; +import { getNonce } from "./getNonce"; +import { Tree } from "./types/Tree"; +import { ImportObj } from "./types/ImportObj"; +import { File } from "@babel/types"; export class Parser { entryFile: string; tree: Tree | undefined; + projectRoot: string; + hasSrcDir: boolean; constructor(filePath: string) { - // Fix when selecting files in wsl file system - this.entryFile = filePath; - if (process.platform === 'linux' && this.entryFile.includes('wsl$')) { - this.entryFile = path.resolve( - filePath.split(path.win32.sep).join(path.posix.sep) - ); - this.entryFile = '/' + this.entryFile.split('/').slice(3).join('/'); - // Fix for when running wsl but selecting files held on windows file system - } else if ( - process.platform === 'linux' && - /[a-zA-Z]/.test(this.entryFile[0]) - ) { - const root = `/mnt/${this.entryFile[0].toLowerCase()}`; - this.entryFile = path.join( - root, - filePath.split(path.win32.sep).slice(1).join(path.posix.sep) - ); - } - + this.entryFile = this.normalizePath(filePath); this.tree = undefined; - // Break down and reasemble given filePath safely for any OS using path? + this.projectRoot = this.findProjectRoot(this.entryFile); + this.hasSrcDir = fs.existsSync(path.join(this.projectRoot, "src")); + } + + private normalizePath(filePath: string): string { + if (process.platform === "linux") { + if (filePath.includes("wsl$")) { + return ( + "/" + filePath.split(path.win32.sep).slice(3).join(path.posix.sep) + ); + // Fix for when running wsl but selecting files held on windows file system + } else if (/[a-zA-Z]/.test(filePath[0])) { + const root = `/mnt/${filePath[0].toLowerCase()}`; + return path.join( + root, + filePath.split(path.win32.sep).slice(1).join(path.posix.sep) + ); + } + } + return filePath; + } + // finds project root directory, the ditectory where "package.json" exists + private findProjectRoot(filePath: string): string { + let currentDir = path.dirname(filePath); + while (currentDir !== path.parse(currentDir).root) { + if (fs.existsSync(path.join(currentDir, "package.json"))) { + return currentDir; + } + currentDir = path.dirname(currentDir); + } + throw new Error("Project root not found"); } - // Public method to generate component tree based on current entryFile public parse(): Tree { + const rootImportPath = this.hasSrcDir ? "./src/app/" : "./app/"; // Create root Tree node const root = { id: getNonce(), - name: path.basename(this.entryFile).replace(/\.(t|j)sx?$/, ''), + name: path.basename(this.entryFile).replace(/\.(t|j)sx?$/, ""), fileName: path.basename(this.entryFile), filePath: this.entryFile, - importPath: '/', // this.entryFile here breaks windows file path on root e.g. C:\\ is detected as third party + importPath: rootImportPath, expanded: false, depth: 0, count: 1, @@ -52,10 +67,11 @@ export class Parser { children: [], parentList: [], props: {}, - error: '', + error: "", }; this.tree = root; + console.log(this.tree); this.parser(root); return this.tree; } @@ -141,26 +157,12 @@ export class Parser { // Recursively builds the React component tree structure starting from root node private parser(componentTree: Tree): Tree | undefined { - // If import is a node module, do not parse any deeper - if (!['\\', '/', '.'].includes(componentTree.importPath[0])) { - componentTree.thirdParty = true; - if ( - componentTree.fileName === 'react-router-dom' || - componentTree.fileName === 'react-router' - ) { - componentTree.reactRouter = true; - } - return; - } - - // Check that file has valid fileName/Path, if not found, add error to node and halt - const fileName = this.getFileName(componentTree); - if (!fileName) { - componentTree.error = 'File not found.'; - return; - } + // const fileName = this.getFileName(componentTree); + // if (!fileName) { + // componentTree.error = "File not found."; + // return; + // } - // If current node recursively calls itself, do not parse any deeper: if (componentTree.parentList.includes(componentTree.filePath)) { return; } @@ -169,15 +171,15 @@ export class Parser { let ast: babelParser.ParseResult; try { ast = babelParser.parse( - fs.readFileSync(path.resolve(componentTree.filePath), 'utf-8'), + fs.readFileSync(path.resolve(componentTree.filePath), "utf-8"), { - sourceType: 'module', + sourceType: "module", tokens: true, - plugins: ['jsx', 'typescript'], + plugins: ["jsx", "typescript"], } ); } catch (err) { - componentTree.error = 'Error while processing this file/node'; + componentTree.error = "Error while processing this file/node"; return componentTree; } @@ -186,11 +188,12 @@ export class Parser { // Get any JSX Children of current file: if (ast.tokens) { - componentTree.children = this.getJSXChildren( + const childrenObj = this.getJSXChildren( ast.tokens, imports, componentTree ); + componentTree.children = Object.values(childrenObj); } // Check if current node is connected to the Redux store @@ -204,70 +207,111 @@ export class Parser { return componentTree; } - // Finds files where import string does not include a file extension - private getFileName(componentTree: Tree): string | undefined { - const ext = path.extname(componentTree.filePath); - let fileName: string | undefined = componentTree.fileName; - - if (!ext) { - // Try and find file extension that exists in directory: - const fileArray = fs.readdirSync(path.dirname(componentTree.filePath)); - const regEx = new RegExp(`${componentTree.fileName}.(j|t)sx?$`); - fileName = fileArray.find((fileStr) => fileStr.match(regEx)); - fileName ? (componentTree.filePath += path.extname(fileName)) : null; - } - - return fileName; - } - // Extracts Imports from current file // const Page1 = lazy(() => import('./page1')); -> is parsed as 'ImportDeclaration' // import Page2 from './page2'; -> is parsed as 'VariableDeclaration' private getImports(body: { [key: string]: any }[]): ImportObj { const bodyImports = body.filter( - (item) => item.type === 'ImportDeclaration' || 'VariableDeclaration' + (item) => item.type === "ImportDeclaration" || "VariableDeclaration" ); - // console.log('bodyImports are: ', bodyImports); + return bodyImports.reduce((accum, curr) => { // Import Declarations: - if (curr.type === 'ImportDeclaration') { + if (curr.type === "ImportDeclaration") { curr.specifiers.forEach( - (i: { - local: { name: string | number }; - imported: { name: any }; - }) => { + (i: { local: { name: string }; imported: { name: string } }) => { + const { importPath, filePath } = this.resolveImportPath( + curr.source.value + ); accum[i.local.name] = { - importPath: curr.source.value, + importPath, + filePath, importName: i.imported ? i.imported.name : i.local.name, }; } ); - } - // Imports Inside Variable Declarations: // Not easy to deal with nested objects - if (curr.type === 'VariableDeclaration') { + // Imports Inside Variable Declarations: // Not easy to deal with nested objects + } else if (curr.type === "VariableDeclaration") { const importPath = this.findVarDecImports(curr.declarations[0]); - if (importPath) { + if (typeof importPath === "string") { const importName = curr.declarations[0].id.name; + const { importPath: resolvedImportPath, filePath } = + this.resolveImportPath(importPath); accum[curr.declarations[0].id.name] = { - importPath, + importPath: resolvedImportPath, + filePath, importName, }; } } return accum; - }, {}); + }, {} as ImportObj); + } + + // Finds files name and file extension + private resolveImportPath(importPath: string): { + importPath: string; + filePath: string; + } { + if (importPath.startsWith("@/")) { + const relativePath = importPath.slice(2); + const importPathResolved = this.hasSrcDir + ? `./src/${relativePath}` + : `./${relativePath}`; + const filePathResolved = path.join( + this.projectRoot, + this.hasSrcDir ? "src" : "", + relativePath + ); + return { + importPath: importPathResolved, + filePath: this.addFileExtension(filePathResolved), + }; + } else if (importPath.startsWith("./") || importPath.startsWith("../")) { + const filePathResolved = path.resolve( + path.dirname(this.entryFile), + importPath + ); + const importPathResolved = path.relative( + this.projectRoot, + filePathResolved + ); + return { + importPath: `./${importPathResolved.replace(/\\/g, "/")}`, + filePath: this.addFileExtension(filePathResolved), + }; + } else if (importPath.startsWith("/")) { + const filePathResolved = path.join(this.projectRoot, importPath); + return { + importPath, + filePath: this.addFileExtension(filePathResolved), + }; + } else { + // Third-party import + return { importPath, filePath: importPath }; + } + } + + private addFileExtension(filePath: string): string { + const extensions = [".tsx", ".ts", ".jsx", ".js"]; + for (const ext of extensions) { + if (fs.existsSync(`${filePath}${ext}`)) { + return `${filePath}${ext}`; + } + } + return filePath; } // Recursive helper method to find import path in Variable Declaration private findVarDecImports(ast: { [key: string]: any }): string | boolean { // Base Case, find import path in variable declaration and return it, - if (ast.hasOwnProperty('callee') && ast.callee.type === 'Import') { + if (ast.hasOwnProperty("callee") && ast.callee.type === "Import") { return ast.arguments[0].value; } // Otherwise look for imports in any other non null/undefined objects in the tree: for (let key in ast) { - if (ast.hasOwnProperty(key) && typeof ast[key] === 'object' && ast[key]) { + if (ast.hasOwnProperty(key) && typeof ast[key] === "object" && ast[key]) { const importPath = this.findVarDecImports(ast[key]); if (importPath) { return importPath; @@ -283,47 +327,90 @@ export class Parser { astTokens: any[], importsObj: ImportObj, parentNode: Tree - ): Tree[] { - let childNodes: { [key: string]: Tree } = {}; + ): { [fileName: string]: Tree } { + let childNodes: { [fileName: string]: Tree } = {}; let props: { [key: string]: boolean } = {}; - let token: { [key: string]: any }; + let currentElement: string | null = null; for (let i = 0; i < astTokens.length; i++) { + const token = astTokens[i]; + // Case for finding JSX tags eg - if ( - astTokens[i].type.label === 'jsxTagStart' && - astTokens[i + 1].type.label === 'jsxName' && - importsObj[astTokens[i + 1].value] - ) { - token = astTokens[i + 1]; - props = this.getJSXProps(astTokens, i + 2); - childNodes = this.getChildNodes( - importsObj, - token, - props, - parentNode, - childNodes - ); + if (token.type.label === "jsxTagStart") { + currentElement = null; + props = {}; + } - // Case for finding components passed in as props e.g. - } else if ( - astTokens[i].type.label === 'jsxName' && - (astTokens[i].value === 'component' || - astTokens[i].value === 'children') && - importsObj[astTokens[i + 3].value] - ) { - token = astTokens[i + 3]; - childNodes = this.getChildNodes( - importsObj, - token, - props, - parentNode, - childNodes - ); + // JSX element name + if (token.type.label === "jsxName" && currentElement === null) { + currentElement = token.value; + // Check if this element is an imported component + if (importsObj[currentElement]) { + childNodes = this.getChildNodes( + importsObj, + { value: currentElement }, + props, + parentNode, + childNodes + ); + } + } + + // JSX props + if (token.type.label === "jsxName" && currentElement !== null) { + const propName = token.value; + if (astTokens[i + 1].type.label === "eq") { + props[propName] = true; + + // Check for component passed as prop + if ( + astTokens[i + 2].type.label === "jsxTagStart" && + astTokens[i + 3].type.label === "jsxName" && + importsObj[astTokens[i + 3].value] + ) { + childNodes = this.getChildNodes( + importsObj, + { value: astTokens[i + 3].value }, + {}, + parentNode, + childNodes + ); + } + } + } + + // Handle components in JSX expressions + if (token.type.label === "jsxExpressionStart") { + let j = i + 1; + while ( + j < astTokens.length && + astTokens[j].type.label !== "jsxExpressionEnd" + ) { + if ( + astTokens[j].type.label === "name" && + importsObj[astTokens[j].value] + ) { + childNodes = this.getChildNodes( + importsObj, + { value: astTokens[j].value }, + {}, + parentNode, + childNodes + ); + } + j++; + } + i = j; // Skip to end of expression + } + + // End of JSX element + if (token.type.label === "jsxTagEnd") { + currentElement = null; + props = {}; } } - return Object.values(childNodes); + return childNodes; } private getChildNodes( @@ -333,37 +420,44 @@ export class Parser { parent: Tree, children: { [key: string]: Tree } ): { [key: string]: Tree } { - if (children[astToken.value]) { - children[astToken.value].count += 1; - children[astToken.value].props = { - ...children[astToken.value].props, + const uniqueChildren: { [fileName: string]: Tree } = {}; + + Object.entries(children).forEach(([key, child]) => { + uniqueChildren[child.fileName] = child; + }); + + const importInfo = imports[astToken.value]; + const isThirdParty = !importInfo.importPath.startsWith("."); + const fileName = path.basename(importInfo.filePath); + + if (uniqueChildren[fileName]) { + uniqueChildren[fileName].count += 1; + uniqueChildren[fileName].props = { + ...uniqueChildren[fileName].props, ...props, }; } else { // Add tree node to childNodes if one does not exist - children[astToken.value] = { + uniqueChildren[fileName] = { id: getNonce(), - name: imports[astToken.value]['importName'], - fileName: path.basename(imports[astToken.value]['importPath']), - filePath: path.resolve( - path.dirname(parent.filePath), - imports[astToken.value]['importPath'] - ), - importPath: imports[astToken.value]['importPath'], + name: importInfo.importName, + fileName: fileName, + filePath: importInfo.filePath, + importPath: importInfo.importPath, expanded: false, depth: parent.depth + 1, - thirdParty: false, + thirdParty: isThirdParty, reactRouter: false, reduxConnect: false, count: 1, props: props, children: [], parentList: [parent.filePath].concat(parent.parentList), - error: '', + error: "", }; } - return children; + return uniqueChildren; } // Extracts prop names from a JSX element @@ -372,10 +466,10 @@ export class Parser { j: number ): { [key: string]: boolean } { const props: any = {}; - while (astTokens[j].type.label !== 'jsxTagEnd') { + while (astTokens[j].type.label !== "jsxTagEnd") { if ( - astTokens[j].type.label === 'jsxName' && - astTokens[j + 1].value === '=' + astTokens[j].type.label === "jsxName" && + astTokens[j + 1].value === "=" ) { props[astTokens[j].value] = true; } @@ -391,8 +485,8 @@ export class Parser { let connectAlias; Object.keys(importsObj).forEach((key) => { if ( - importsObj[key].importPath === 'react-redux' && - importsObj[key].importName === 'connect' + importsObj[key].importPath === "react-redux" && + importsObj[key].importName === "connect" ) { reduxImported = true; connectAlias = key; @@ -406,8 +500,8 @@ export class Parser { // Check that connect method is invoked and exported in the file for (let i = 0; i < astTokens.length; i += 1) { if ( - astTokens[i].type.label === 'export' && - astTokens[i + 1].type.label === 'default' && + astTokens[i].type.label === "export" && + astTokens[i + 1].type.label === "default" && astTokens[i + 2].value === connectAlias ) { return true; @@ -415,4 +509,4 @@ export class Parser { } return false; } -} \ No newline at end of file +} diff --git a/src/types/ImportObj.ts b/src/types/ImportObj.ts index a67a46f3b..05b01c28a 100644 --- a/src/types/ImportObj.ts +++ b/src/types/ImportObj.ts @@ -1,3 +1,3 @@ export type ImportObj = { - [key: string]: { importPath: string; importName: string; }; + [key: string]: { importPath: string; importName: string; filePath: string }; }; From f89702a7d95f9af84d993830033e25249f1ae62a Mon Sep 17 00:00:00 2001 From: Katalogo262f Date: Sat, 21 Sep 2024 19:34:54 +0530 Subject: [PATCH 2/2] feat: Nextjs file structure. feat: Duplicate child handled --- src/parser.ts | 39 ++++++++++++++++----------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index 9bc233fb3..e699ef3d6 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,4 +1,3 @@ -//if multiple children of a parent have the same fileName/pathName then only keep one of them, like a set(). and if children have the same fileName/pathName but different parent then import * as babelParser from "@babel/parser"; import * as path from "path"; import * as fs from "fs"; @@ -157,12 +156,6 @@ export class Parser { // Recursively builds the React component tree structure starting from root node private parser(componentTree: Tree): Tree | undefined { - // const fileName = this.getFileName(componentTree); - // if (!fileName) { - // componentTree.error = "File not found."; - // return; - // } - if (componentTree.parentList.includes(componentTree.filePath)) { return; } @@ -461,22 +454,22 @@ export class Parser { } // Extracts prop names from a JSX element - private getJSXProps( - astTokens: { [key: string]: any }[], - j: number - ): { [key: string]: boolean } { - const props: any = {}; - while (astTokens[j].type.label !== "jsxTagEnd") { - if ( - astTokens[j].type.label === "jsxName" && - astTokens[j + 1].value === "=" - ) { - props[astTokens[j].value] = true; - } - j += 1; - } - return props; - } + // private getJSXProps( + // astTokens: { [key: string]: any }[], + // j: number + // ): { [key: string]: boolean } { + // const props: any = {}; + // while (astTokens[j].type.label !== "jsxTagEnd") { + // if ( + // astTokens[j].type.label === "jsxName" && + // astTokens[j + 1].value === "=" + // ) { + // props[astTokens[j].value] = true; + // } + // j += 1; + // } + // return props; + // } // Checks if current Node is connected to React-Redux Store private checkForRedux(astTokens: any[], importsObj: ImportObj): boolean {