import { parse, ParserOptions } from '@babel/parser';
import * as babelTraverse from '@babel/traverse';
import type { TraverseOptions } from '@babel/traverse';
import * as t from '@babel/types';
import type {
ParsedComponent,
ComponentChild,
PropEntry,
ExtractedStyles,
ImportInfo
} from '../types/index.js';
import { categorizeTailwindStyles } from './tailwind.js';
import { categorizeStyledStyles, detectStyledComponents } from './styled.js';
import { categorizeCSSModuleStyles, detectCSSModules } from './css-modules.js';
// Handle both ESM and CJS module exports for @babel/traverse
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const babelModule = babelTraverse as any;
const traverse = (
babelModule.default?.default ||
babelModule.default ||
babelModule
) as (ast: t.Node, opts: TraverseOptions) => void;
const PARSER_OPTIONS: ParserOptions = {
sourceType: 'module',
plugins: [
'jsx',
'typescript',
'decorators-legacy',
'classProperties',
'optionalChaining',
'nullishCoalescingOperator'
]
};
function extractPropValue(node: t.Node): string {
if (t.isStringLiteral(node)) {
return node.value;
}
if (t.isNumericLiteral(node)) {
return String(node.value);
}
if (t.isBooleanLiteral(node)) {
return String(node.value);
}
if (t.isJSXExpressionContainer(node)) {
return extractPropValue(node.expression);
}
if (t.isIdentifier(node)) {
return `{${node.name}}`;
}
if (t.isTemplateLiteral(node)) {
// Simplified template literal extraction
const parts = node.quasis.map((q, i) => {
const expr = node.expressions[i];
const exprStr = expr ? `\${...}` : '';
return q.value.raw + exprStr;
});
return `\`${parts.join('')}\``;
}
if (t.isMemberExpression(node)) {
const obj = t.isIdentifier(node.object) ? node.object.name : '...';
const prop = t.isIdentifier(node.property) ? node.property.name : '...';
return `{${obj}.${prop}}`;
}
if (t.isCallExpression(node)) {
const callee = t.isIdentifier(node.callee) ? node.callee.name : '...';
return `{${callee}(...)}`;
}
if (t.isArrowFunctionExpression(node) || t.isFunctionExpression(node)) {
return '{() => ...}';
}
if (t.isObjectExpression(node)) {
return '{...object}';
}
if (t.isArrayExpression(node)) {
return '[...array]';
}
if (t.isConditionalExpression(node)) {
return '{...? ... : ...}';
}
if (t.isLogicalExpression(node)) {
return `{... ${node.operator} ...}`;
}
return '{expression}';
}
function extractPropsFromElement(node: t.JSXOpeningElement): PropEntry[] {
const props: PropEntry[] = [];
for (const attr of node.attributes) {
if (t.isJSXAttribute(attr)) {
const name = t.isJSXIdentifier(attr.name)
? attr.name.name
: `${attr.name.namespace.name}:${attr.name.name.name}`;
let value = 'true';
let isExpression = false;
if (attr.value) {
if (t.isStringLiteral(attr.value)) {
value = attr.value.value;
} else if (t.isJSXExpressionContainer(attr.value)) {
value = extractPropValue(attr.value.expression);
isExpression = true;
}
}
props.push({ name, value, isExpression });
} else if (t.isJSXSpreadAttribute(attr)) {
props.push({
name: '...spread',
value: extractPropValue(attr.argument),
isExpression: true
});
}
}
return props;
}
function extractTextContent(children: t.Node[]): string | undefined {
const textParts: string[] = [];
for (const child of children) {
if (t.isJSXText(child)) {
const text = child.value.trim();
if (text) {
textParts.push(text);
}
} else if (t.isJSXExpressionContainer(child)) {
if (t.isStringLiteral(child.expression)) {
textParts.push(child.expression.value);
} else if (!t.isJSXEmptyExpression(child.expression)) {
textParts.push('{expression}');
}
}
}
return textParts.length > 0 ? textParts.join(' ') : undefined;
}
function parseJSXElement(node: t.JSXElement): ComponentChild {
const opening = node.openingElement;
let name: string;
if (t.isJSXIdentifier(opening.name)) {
name = opening.name.name;
} else if (t.isJSXMemberExpression(opening.name)) {
const obj = opening.name.object;
const prop = opening.name.property;
name = `${t.isJSXIdentifier(obj) ? obj.name : '...'}.${prop.name}`;
} else {
name = 'unknown';
}
const props = extractPropsFromElement(opening);
const children: ComponentChild[] = [];
const textContent = extractTextContent(node.children);
for (const child of node.children) {
if (t.isJSXElement(child)) {
children.push(parseJSXElement(child));
} else if (t.isJSXFragment(child)) {
// Process fragment children
for (const fragChild of child.children) {
if (t.isJSXElement(fragChild)) {
children.push(parseJSXElement(fragChild));
}
}
}
}
return {
name,
props,
children,
textContent,
location: {
start: {
line: node.loc?.start.line ?? 0,
column: node.loc?.start.column ?? 0
},
end: {
line: node.loc?.end.line ?? 0,
column: node.loc?.end.column ?? 0
}
}
};
}
function extractImports(ast: t.File): ImportInfo[] {
const imports: ImportInfo[] = [];
traverse(ast, {
ImportDeclaration(path: { node: t.ImportDeclaration }) {
const source = path.node.source.value;
const specifiers: string[] = [];
let isDefault = false;
for (const spec of path.node.specifiers) {
if (t.isImportDefaultSpecifier(spec)) {
specifiers.push(spec.local.name);
isDefault = true;
} else if (t.isImportSpecifier(spec)) {
const imported = t.isIdentifier(spec.imported)
? spec.imported.name
: spec.imported.value;
specifiers.push(imported);
} else if (t.isImportNamespaceSpecifier(spec)) {
specifiers.push(`* as ${spec.local.name}`);
}
}
const isStyleModule = /\.module\.(css|scss|sass|less)$/.test(source) ||
source === 'styled-components' ||
source.endsWith('.css');
imports.push({
source,
specifiers,
isDefault,
isStyleModule
});
}
});
return imports;
}
function mergeStyles(
...styleSources: { colors: any[]; spacing: any[]; typography: any[]; other: any[] }[]
): ExtractedStyles {
return {
colors: styleSources.flatMap(s => s.colors),
spacing: styleSources.flatMap(s => s.spacing),
typography: styleSources.flatMap(s => s.typography),
other: styleSources.flatMap(s => s.other)
};
}
export function parseComponent(code: string, filePath: string): ParsedComponent {
const ast = parse(code, PARSER_OPTIONS);
// Extract component name
let componentName = 'UnknownComponent';
let rootElement: ComponentChild | null = null;
const componentProps: PropEntry[] = [];
// Find the main component
traverse(ast, {
// Function component
FunctionDeclaration(path: { node: t.FunctionDeclaration }) {
if (path.node.id && /^[A-Z]/.test(path.node.id.name)) {
componentName = path.node.id.name;
// Extract props from parameters
const params = path.node.params;
if (params.length > 0 && t.isObjectPattern(params[0])) {
for (const prop of params[0].properties) {
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
componentProps.push({
name: prop.key.name,
value: 'prop',
isExpression: false
});
}
}
}
}
},
// Arrow function component
VariableDeclarator(path: { node: t.VariableDeclarator }) {
if (
t.isIdentifier(path.node.id) &&
/^[A-Z]/.test(path.node.id.name) &&
(t.isArrowFunctionExpression(path.node.init) ||
t.isFunctionExpression(path.node.init))
) {
componentName = path.node.id.name;
const func = path.node.init;
if (func.params.length > 0 && t.isObjectPattern(func.params[0])) {
for (const prop of func.params[0].properties) {
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
componentProps.push({
name: prop.key.name,
value: 'prop',
isExpression: false
});
}
}
}
}
},
// Find return statement with JSX
ReturnStatement(path: { node: t.ReturnStatement }) {
if (t.isJSXElement(path.node.argument) && !rootElement) {
rootElement = parseJSXElement(path.node.argument);
} else if (t.isJSXFragment(path.node.argument) && !rootElement) {
// Handle fragments
const children: ComponentChild[] = [];
for (const child of path.node.argument.children) {
if (t.isJSXElement(child)) {
children.push(parseJSXElement(child));
}
}
rootElement = {
name: 'Fragment',
props: [],
children,
location: {
start: { line: 0, column: 0 },
end: { line: 0, column: 0 }
}
};
}
}
});
// Extract styles from various sources
let styles: ExtractedStyles = {
colors: [],
spacing: [],
typography: [],
other: []
};
// Collect all className values for Tailwind
const classNames: string[] = [];
traverse(ast, {
JSXAttribute(path: { node: t.JSXAttribute }) {
if (
t.isJSXIdentifier(path.node.name) &&
path.node.name.name === 'className'
) {
if (t.isStringLiteral(path.node.value)) {
classNames.push(path.node.value.value);
}
}
}
});
if (classNames.length > 0) {
const tailwindStyles = categorizeTailwindStyles(classNames.join(' '));
styles = mergeStyles(styles, tailwindStyles);
}
// Styled components
if (detectStyledComponents(code)) {
const styledStyles = categorizeStyledStyles(code);
styles = mergeStyles(styles, styledStyles);
}
// CSS Modules
if (detectCSSModules(code)) {
const cssModuleStyles = categorizeCSSModuleStyles(code, filePath);
styles = mergeStyles(styles, cssModuleStyles);
}
const imports = extractImports(ast);
return {
name: componentName,
children: rootElement ? [rootElement] : [],
props: componentProps,
styles,
imports
};
}
export function extractAllChildren(component: ParsedComponent): ComponentChild[] {
const allChildren: ComponentChild[] = [];
function traverse(children: ComponentChild[]) {
for (const child of children) {
allChildren.push(child);
if (child.children.length > 0) {
traverse(child.children);
}
}
}
traverse(component.children);
return allChildren;
}