Skip to content

Added support for barrel files and extensionless imports. This can be… #22

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

<br />

Expand Down
2 changes: 1 addition & 1 deletion extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
252 changes: 242 additions & 10 deletions extension/src/SaplingParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -23,13 +23,40 @@ 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<string, string[]>)){
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?$/, ''),
fileName: path.basename(entryFile),
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;
Expand Down Expand Up @@ -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' ||
Expand Down Expand Up @@ -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,
Expand All @@ -175,6 +214,107 @@ const ASTParser = {
return childNodes;
},

recursivelySearchBarrelFiles(filePath: string, componentName: string, topBarrelFile: boolean = true): {filePath: string, props?: Record<string, boolean>} {
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[],
Expand Down Expand Up @@ -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<string, boolean> {
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<string, boolean> {
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 = {};
// barrel files, namespace imports, default import + namespace/named imports, require invocations/method calls, ...
11 changes: 8 additions & 3 deletions extension/src/SaplingTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export class Tree implements IRawNode, INode {
fileName: string;
filePath: string;
importPath: string;
aliases: Record<string, string>;
expanded: boolean;
depth: number;
count: number;
Expand All @@ -19,6 +20,7 @@ export class Tree implements IRawNode, INode {
parent: Tree;
parentList: string[];
props: Record<string, boolean>;
projectBaseURL: string | undefined;
error:
| ''
| 'File not found.'
Expand All @@ -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 ?? '';
}

Expand Down Expand Up @@ -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);
}
}
8 changes: 8 additions & 0 deletions extension/src/types/ExportData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { FunctionDeclaration, VariableDeclarator, ArrowFunctionExpression } from "@babel/types";

export type ExportData = {
exportPath?: string;
exportName?: string;
declaration?: FunctionDeclaration | ArrowFunctionExpression;
default?: boolean;
};
3 changes: 2 additions & 1 deletion extension/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from '../SaplingTree';
export * from './ImportData';
export * from './TreeNode';
export * from './Token';
export * from './StoreTypes';
export * from './StoreTypes';
export * from './ExportData';
Loading