diff --git a/README.md b/README.md
index b807b32..0110f52 100644
--- a/README.md
+++ b/README.md
@@ -186,9 +186,10 @@ Any updates you make to your files will be automatically processed and mirrored
1. Fork the [project repo](https://github.com/hb1998/react-component-tree).
2. Open the `react-component-tree/extension` folder in your VS Code IDE.
3. Open `extension/src/extension.ts`
-4. With the `extension` folder as your pwd, run this command: `npm run watch`.
-5. From the menu, click Run - Start Debugging (or press F5), and select VS Code Extension Development from the command palette dropdown. An extension development host will open in a new window.
-6. Click the React Component Tree icon on the extension development host window sidebar. To refresh the extension development host, use `Ctrl+R` (or `Cmd+R` on Mac).
+4. Run `npm i`
+5. With the `extension` folder as your pwd, run this command: `npm run watch`.
+6. From the menu, click Run - Start Debugging (or press F5), and select VS Code Extension Development from the command palette dropdown. An extension development host will open in a new window.
+7. Click the React Component Tree icon on the extension development host window sidebar. To refresh the extension development host, use `Ctrl+R` (or `Cmd+R` on Mac).
diff --git a/extension/package.json b/extension/package.json
index cf030b3..b19f625 100644
--- a/extension/package.json
+++ b/extension/package.json
@@ -5,7 +5,7 @@
"repository": "https://github.com/hb1998/react-component-tree",
"icon": "media/logo-128px.png",
"publisher": "HabeebArul",
- "version": "1.1.1",
+ "version": "1.2.0",
"engines": {
"vscode": "^1.60.0"
},
diff --git a/extension/src/SaplingParser.ts b/extension/src/SaplingParser.ts
index 244410f..cecbb5d 100644
--- a/extension/src/SaplingParser.ts
+++ b/extension/src/SaplingParser.ts
@@ -3,13 +3,13 @@ import * as path from 'path';
import { parse as babelParse } from '@babel/parser';
import {
- ImportDeclaration, isArrayPattern, isCallExpression, isIdentifier, isImport,
- isImportDeclaration, isImportDefaultSpecifier, isImportNamespaceSpecifier, isImportSpecifier,
- isObjectPattern, isObjectProperty, isStringLiteral, isVariableDeclaration, Node as ASTNode,
- VariableDeclaration
+ ImportDeclaration, FunctionDeclaration, isArrayPattern, isCallExpression, isIdentifier, isImport,
+ isImportDeclaration, isExportDeclaration, isExportAllDeclaration, isExportNamedDeclaration, isFunctionDeclaration, isImportDefaultSpecifier, isImportNamespaceSpecifier, isImportSpecifier,
+ isObjectPattern, isObjectProperty, isStringLiteral, isVariableDeclaration, isTSTypeAnnotation, isArrowFunctionExpression, Node as ASTNode,
+ VariableDeclaration, ExportAllDeclaration, ExportNamedDeclaration, ExportDefaultDeclaration,isExportDefaultDeclaration, isTSTypeAliasDeclaration, isTSTypeLiteral, isTSPropertySignature, isVariableDeclarator, ArrowFunctionExpression, is
} from '@babel/types';
-import { ImportData, Token, Tree } from './types';
+import { ExportData, ImportData, Token, Tree } from './types';
export class SaplingParser {
/** Public method to generate component tree based on entry file or input node.
@@ -23,6 +23,31 @@ export class SaplingParser {
public static parse(input: unknown): unknown {
if (typeof input === 'string') {
const entryFile = ParserHelpers.processFilePath(input);
+ let baseFilePath = path.dirname(entryFile);
+ const aliases = {};
+ for(let i = 0; i < 10; i++) {
+ const fileArray = fs.readdirSync(baseFilePath);
+ if(fileArray.includes('tsconfig.json')){
+ const tsConfigCompilerOptions = JSON.parse(fs.readFileSync(path.join(baseFilePath, 'tsconfig.json'), 'utf-8').split('\n').filter((line)=>{
+ return !line.includes('//');
+ }).join('\n')).compilerOptions;
+ if(tsConfigCompilerOptions.baseUrl){
+ baseFilePath = path.join(baseFilePath, tsConfigCompilerOptions.baseUrl);
+ }
+ if(tsConfigCompilerOptions.paths){
+ for(const [key, value] of Object.entries(tsConfigCompilerOptions.paths as Record)){
+ if(value.length > 0){
+ aliases[key] = value[0];
+ }
+ }
+ }
+ break;
+ }
+ else if(fileArray.includes('package.json')){
+ break;
+ }
+ baseFilePath = path.join(baseFilePath, '..');
+ }
// Create root Tree node
const root = new Tree({
name: path.basename(entryFile).replace(/\.[jt]sx?$/, ''),
@@ -30,6 +55,8 @@ export class SaplingParser {
filePath: entryFile,
importPath: '/', // this.entryFile here breaks windows file path on root e.g. C:\\ is detected as third party
parent: null,
+ aliases,
+ projectBaseURL: baseFilePath
});
ASTParser.parser(root);
return root;
@@ -81,7 +108,7 @@ const ASTParser = {
parser(root: Tree): void {
const recurse = (componentTree: Tree): void => {
// If import is a node module, do not parse any deeper
- if (!['\\', '/', '.'].includes(componentTree.importPath[0])) {
+ if (!['\\', '/', '.'].includes(componentTree.importPath[0]) && !componentTree.aliases[componentTree.importPath]) {
componentTree.set('thirdParty', true);
if (
componentTree.fileName === 'react-router-dom' ||
@@ -157,9 +184,21 @@ const ASTParser = {
} else {
const moduleIdentifier = imports[astToken.value].importPath;
const name = imports[astToken.value].importName;
- const filePath = ParserHelpers.validateFilePath(
- path.resolve(path.dirname(parent.filePath), moduleIdentifier)
+ let filePath = ParserHelpers.validateFilePath(
+ parent.aliases[moduleIdentifier] ? path.join(parent.projectBaseURL, parent.aliases[moduleIdentifier]) : path.resolve(path.dirname(parent.filePath), moduleIdentifier)
);
+ if(parent.aliases[moduleIdentifier] || ['\\', '/', '.'].includes(moduleIdentifier[0])){
+ try{
+ const barrelFileSearchResults = ASTParser.recursivelySearchBarrelFiles(filePath, name);
+ filePath = barrelFileSearchResults.filePath;
+ if(barrelFileSearchResults.props){
+ Object.assign(props, barrelFileSearchResults.props);
+ }
+ }
+ catch(e){
+ console.error('problem in recursivelySearchBarrelFiles:' + e);
+ }
+ };
// Add tree node to childNodes if one does not exist
childNodes[astToken.value] = new Tree({
name,
@@ -175,6 +214,107 @@ const ASTParser = {
return childNodes;
},
+ recursivelySearchBarrelFiles(filePath: string, componentName: string, topBarrelFile: boolean = true): {filePath: string, props?: Record} {
+ const extensions = ['.tsx', '.ts', '.jsx', '.js'];
+ const barrelFileNames = extensions.map((ext) => `index${ext}`);
+ const fileName = filePath.substring(filePath.lastIndexOf('/') + 1);
+ const parent = filePath.substring(0, filePath.lastIndexOf('/'));
+ // If it does not have an extension, check for all possible extensions
+ if(!fs.existsSync(filePath)){
+ if(fileName.lastIndexOf('.') === -1){
+ for(const ext of extensions){
+ if(fs.existsSync(path.join(parent, `${fileName}${ext}`))){
+ return ASTParser.recursivelySearchBarrelFiles(path.join(parent, `${fileName}${ext}`), componentName, topBarrelFile);
+ }
+ }
+ }
+ }
+
+ // If it is a directory, check for barrel files
+ if(fs.lstatSync(filePath).isDirectory()){
+ const files = fs.readdirSync(filePath);
+ for(const barrelFileName of barrelFileNames){
+ if(files.includes(barrelFileName)){
+ return ASTParser.recursivelySearchBarrelFiles(path.join(filePath, barrelFileName), componentName, topBarrelFile);
+ }
+ }
+ }
+ else {
+ const ast = babelParse(fs.readFileSync(filePath, 'utf-8'), {
+ sourceType: 'module',
+ tokens: true, // default: false, tokens deprecated from babel v7
+ plugins: ['jsx', 'typescript'],
+ // TODO: additional plugins to look into supporting for future releases
+ // 'importMeta': https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import.meta
+ // 'importAssertions': parses ImportAttributes type
+ // https://github.com/babel/babel/blob/main/packages/babel-parser/ast/spec.md#ImportAssertions
+ allowImportExportEverywhere: true, // enables parsing dynamic imports and exports in body
+ attachComment: false, // performance benefits
+ });
+ const exportDataArray = ExportParser.parse(ast.program.body);
+ // if index file
+ if(barrelFileNames.includes(filePath.substring(filePath.lastIndexOf('/') + 1))){
+ for(const exportData of exportDataArray){
+ const componentFouldFilePath = ASTParser.recursivelySearchBarrelFiles(path.join(filePath,'..', exportData.exportPath), componentName, false);
+ if(componentFouldFilePath){
+ return componentFouldFilePath;
+ }
+ }
+ // If file is not found in the barrel file, throw an error
+ if(topBarrelFile){
+ // console.error('FILE NOT FOUND:', {filePath, componentName, exportDataArray});
+ return {filePath};
+ }
+ }
+ // We have a file with an extension
+ else {
+ if(exportDataArray.length > 0){
+ let foundExportData: ExportData;
+ for(const exportData of exportDataArray){
+ if(exportData.exportName === componentName){
+ foundExportData = exportData;
+ }
+ }
+ const defaultIndex = exportDataArray.findIndex((exportData)=>{
+ return exportData.default;
+ });
+ if(defaultIndex !== -1 && topBarrelFile){
+ foundExportData = exportDataArray[defaultIndex];
+ }
+ if(foundExportData){
+ if(foundExportData.declaration){
+ return {filePath, props: DestructuredPropsParser.parse(foundExportData.declaration)};
+ }
+ // If file has a default export that is an identifier
+ else if(foundExportData.exportName){
+ const foundFn = ASTParser.findIdentifierReference(ast.program.body, foundExportData.exportName);
+ if(foundFn){
+ return {filePath, props: DestructuredPropsParser.parse(foundFn)};
+ }
+ }
+ return {filePath};
+ }
+ }
+ }
+ }
+ },
+
+ findIdentifierReference(body: ASTNode[], identifier: string): ArrowFunctionExpression | FunctionDeclaration | undefined {
+ for(const node of body){
+ if(isFunctionDeclaration(node) && node.id && node.id.name === identifier){
+ return node;
+ }
+ if(isVariableDeclaration(node)){
+ for(const declaration of node.declarations){
+ if(isVariableDeclarator(declaration) && isIdentifier(declaration.id) && declaration.id.name === identifier && (isFunctionDeclaration(declaration.init) || isArrowFunctionExpression(declaration.init))){
+ return declaration.init;
+ }
+ }
+ }
+ }
+ return undefined;
+ },
+
// Finds JSX React Components in current file
getJSXChildren(
astTokens: Token[],
@@ -441,8 +581,100 @@ const ImportParser = {
},
};
+const ExportParser = {
+ parse(body: ASTNode[]): ExportData[] {
+ return body
+ .filter((astNode) => isExportDeclaration(astNode))
+ .reduce((accumulator, declaration) => {
+ return [...accumulator, ...(isExportAllDeclaration(declaration)
+ ? [ExportParser.parseExportAllDeclaration(declaration)]
+ : isExportNamedDeclaration(declaration) ? ExportParser.parseExportNamedDeclaration(declaration) : isExportDefaultDeclaration(declaration) ? [ExportParser.parseExportDefaultDeclaration(declaration)] : [])];
+ }, []);
+ },
+
+ parseExportDefaultDeclaration(declaration: ExportDefaultDeclaration): ExportData {
+ if(isFunctionDeclaration(declaration.declaration) || isArrowFunctionExpression(declaration.declaration)){
+ return {
+ default: true,
+ declaration: declaration.declaration
+ };
+ }
+ if(isIdentifier(declaration.declaration)){
+ return {
+ default: true,
+ exportName: declaration.declaration.name
+ };
+ }
+ return {
+ default: true
+ };
+ },
+
+ parseExportAllDeclaration(declaration: ExportAllDeclaration): ExportData {
+ return {
+ exportPath: declaration.source.value,
+ };
+ },
+
+ parseExportNamedDeclaration(declaration: ExportNamedDeclaration): ExportData[] {
+ if(isFunctionDeclaration(declaration.declaration)){
+ return [{
+ exportName: declaration.declaration.id.name,
+ declaration: declaration.declaration
+ }];
+ }
+ if(isVariableDeclaration(declaration.declaration)){
+ return declaration.declaration.declarations.map((subDeclaration): ExportData=>{
+ if(isIdentifier(subDeclaration.id)){
+ if(isFunctionDeclaration(subDeclaration.init) || isArrowFunctionExpression(subDeclaration.init)){
+ return {
+ exportName: subDeclaration.id.name,
+ declaration: subDeclaration.init
+ };
+ }
+ return {
+ exportName: subDeclaration.id.name
+ };
+ }
+ throw new Error('Only Identifier exports implemented');
+ });
+ }
+ if(isTSTypeAliasDeclaration(declaration.declaration)){
+ return [];
+ }
+ throw new Error('Only Function Declaration and Variable exports implemented');
+ }
+};
+
+const DestructuredPropsParser = {
+ parse(fn: FunctionDeclaration | ArrowFunctionExpression): Record {
+ if(isFunctionDeclaration(fn) || isArrowFunctionExpression(fn)){
+ if(isObjectPattern(fn.params[0])){
+ return DestructuredPropsParser.arrayToObject(fn.params[0].properties.map((prop)=>{
+ if(isObjectProperty(prop) && isIdentifier(prop.key)){
+ return prop.key.name;
+ }
+ }));
+ }
+ else if(isIdentifier(fn.params[0]) && isTSTypeAnnotation(fn.params[0].typeAnnotation) && isTSTypeLiteral(fn.params[0].typeAnnotation.typeAnnotation)){
+ return DestructuredPropsParser.arrayToObject(fn.params[0].typeAnnotation.typeAnnotation.members.map((member)=>{
+ if(isTSPropertySignature(member) && isIdentifier(member.key)){
+ return member.key.name;
+ }
+ }));
+ }
+ }
+ return {};
+ },
+ arrayToObject(props: string[]): Record {
+ return props.reduce((accumulator, prop) => {
+ accumulator[prop] = true;
+ return accumulator;
+ }, {});
+ }
+};
+
// TODO: Follow import source paths and parse Export{Named,Default,All}Declarations
// See: https://github.com/babel/babel/blob/main/packages/babel-parser/ast/spec.md#exports
// Necessary for handling...
-// barrel files, namespace imports, default import + namespace/named imports, require invocations/method calls, ...
-const ExportParser = {};
\ No newline at end of file
+// barrel files, namespace imports, default import + namespace/named imports, require invocations/method calls, ...
\ No newline at end of file
diff --git a/extension/src/SaplingTree.ts b/extension/src/SaplingTree.ts
index 1260d20..eba2072 100644
--- a/extension/src/SaplingTree.ts
+++ b/extension/src/SaplingTree.ts
@@ -9,6 +9,7 @@ export class Tree implements IRawNode, INode {
fileName: string;
filePath: string;
importPath: string;
+ aliases: Record;
expanded: boolean;
depth: number;
count: number;
@@ -19,6 +20,7 @@ export class Tree implements IRawNode, INode {
parent: Tree;
parentList: string[];
props: Record;
+ projectBaseURL: string | undefined;
error:
| ''
| 'File not found.'
@@ -41,6 +43,8 @@ export class Tree implements IRawNode, INode {
this.parent = node?.parent;
this.parentList = node?.parentList ?? [];
this.props = node?.props ?? {};
+ this.aliases = node?.aliases ?? node?.parent?.aliases ?? {};
+ this.projectBaseURL = node?.projectBaseURL ?? node?.parent?.projectBaseURL;
this.error = node?.error ?? '';
}
@@ -236,11 +240,12 @@ export class Tree implements IRawNode, INode {
* @returns Tree class object with all nested descendant nodes also of Tree class.
*/
public static deserialize(data: Tree): Tree {
- const recurse = (node: Tree): Tree =>
- (new Tree({
+ const recurse = (node: Tree): Tree =>{
+ return new Tree({
...node,
children: node.children?.map((child) => recurse(child)),
- }));
+ });
+ };
return recurse(data);
}
}
\ No newline at end of file
diff --git a/extension/src/types/ExportData.ts b/extension/src/types/ExportData.ts
new file mode 100644
index 0000000..246dd8b
--- /dev/null
+++ b/extension/src/types/ExportData.ts
@@ -0,0 +1,8 @@
+import { FunctionDeclaration, VariableDeclarator, ArrowFunctionExpression } from "@babel/types";
+
+export type ExportData = {
+ exportPath?: string;
+ exportName?: string;
+ declaration?: FunctionDeclaration | ArrowFunctionExpression;
+ default?: boolean;
+};
\ No newline at end of file
diff --git a/extension/src/types/index.ts b/extension/src/types/index.ts
index e903e0a..ddaecf6 100644
--- a/extension/src/types/index.ts
+++ b/extension/src/types/index.ts
@@ -2,4 +2,5 @@ export * from '../SaplingTree';
export * from './ImportData';
export * from './TreeNode';
export * from './Token';
-export * from './StoreTypes';
\ No newline at end of file
+export * from './StoreTypes';
+export * from './ExportData';
\ No newline at end of file
diff --git a/extension/src/webviews/components/Tree/Tree.tsx b/extension/src/webviews/components/Tree/Tree.tsx
index 5d5f6cb..43817cf 100644
--- a/extension/src/webviews/components/Tree/Tree.tsx
+++ b/extension/src/webviews/components/Tree/Tree.tsx
@@ -42,10 +42,10 @@ const Tree: React.FC = () => {
if (node) {
scrollToNode(node);
}
- })
+ });
return () => {
PubSub.unsubscribe(subscription);
- }
+ };
}, []);
const scrollToNode = (node: INode) => {