jsc-typescript-ast-mcp 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ Analyze React component structures in your codebase.
|
|
|
97
97
|
- `entryFilePath`: Path to the entry file
|
|
98
98
|
- `maxDepth`: Maximum depth of the component tree (default: 3)
|
|
99
99
|
- `data-id` (optional): Attribute name to capture into `TreeNode.dataId` (example: `data-attribute-id`)
|
|
100
|
-
- **Output**: JSON representation of the component tree structure
|
|
100
|
+
- **Output**: JSON representation of the component tree structure, including optional element metadata such as `props`, `dataId`, and `onClick`
|
|
101
101
|
|
|
102
102
|
## License
|
|
103
103
|
|
|
@@ -1,16 +1,52 @@
|
|
|
1
|
-
import { SyntaxKind } from 'ts-morph';
|
|
2
|
-
|
|
1
|
+
import { Node, SyntaxKind } from 'ts-morph';
|
|
2
|
+
const isComponentVariableDeclaration = (node) => {
|
|
3
|
+
if (!Node.isVariableDeclaration(node)) {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
const initializer = node.getInitializer();
|
|
7
|
+
if (!initializer) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
return (initializer.getKind() === SyntaxKind.ArrowFunction ||
|
|
11
|
+
initializer.getKind() === SyntaxKind.FunctionExpression);
|
|
12
|
+
};
|
|
13
|
+
const isComponentDeclaration = (node) => {
|
|
14
|
+
if (Node.isFunctionDeclaration(node)) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
return isComponentVariableDeclaration(node);
|
|
18
|
+
};
|
|
19
|
+
const getFirstExportedComponent = (sourceFile) => {
|
|
20
|
+
const exportedDeclarations = sourceFile.getExportedDeclarations();
|
|
21
|
+
for (const declarations of exportedDeclarations.values()) {
|
|
22
|
+
const componentDeclaration = declarations.find(isComponentDeclaration);
|
|
23
|
+
if (componentDeclaration) {
|
|
24
|
+
return componentDeclaration;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
};
|
|
3
29
|
export const getComponent = (sourceFile) => {
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
return
|
|
14
|
-
}
|
|
15
|
-
|
|
30
|
+
const defaultExportDeclaration = sourceFile
|
|
31
|
+
.getDefaultExportSymbol()
|
|
32
|
+
?.getDeclarations()
|
|
33
|
+
?.find(isComponentDeclaration);
|
|
34
|
+
if (defaultExportDeclaration) {
|
|
35
|
+
return defaultExportDeclaration;
|
|
36
|
+
}
|
|
37
|
+
const firstExportedComponent = getFirstExportedComponent(sourceFile);
|
|
38
|
+
if (firstExportedComponent) {
|
|
39
|
+
return firstExportedComponent;
|
|
40
|
+
}
|
|
41
|
+
const localVariableComponent = sourceFile
|
|
42
|
+
.getVariableDeclarations()
|
|
43
|
+
.find(isComponentVariableDeclaration);
|
|
44
|
+
if (localVariableComponent) {
|
|
45
|
+
return localVariableComponent;
|
|
46
|
+
}
|
|
47
|
+
const localFunctionComponent = sourceFile.getFunctions()[0];
|
|
48
|
+
if (localFunctionComponent) {
|
|
49
|
+
return localFunctionComponent;
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`Component not found in ${sourceFile.getFilePath()}`);
|
|
16
52
|
};
|
|
@@ -2,7 +2,7 @@ import { Node, SyntaxKind, } from 'ts-morph';
|
|
|
2
2
|
import { analyzeComponent } from './analyzeComponent.js';
|
|
3
3
|
import { getComponent } from './component.js';
|
|
4
4
|
import { getTagName } from './nameHelper.js';
|
|
5
|
-
import { extractPropsFromNode } from './props.js';
|
|
5
|
+
import { extractOnClickInfoFromNode, extractPropsFromNode } from './props.js';
|
|
6
6
|
import { handleTernary } from './ternary.js';
|
|
7
7
|
export const extractFromExpression = (expression, project, options, depth) => {
|
|
8
8
|
if (Node.isConditionalExpression(expression)) {
|
|
@@ -91,19 +91,64 @@ const resolveComponentFile = (node) => {
|
|
|
91
91
|
const defaultImport = imp.getDefaultImport();
|
|
92
92
|
if (defaultImport?.getText() === tagName) {
|
|
93
93
|
const file = imp.getModuleSpecifierSourceFile();
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
if (!file || file.getFilePath().includes('/node_modules/')) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const component = getComponent(file);
|
|
99
|
+
return component ?? null;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
96
104
|
}
|
|
97
105
|
for (const n of named) {
|
|
98
106
|
if (n.getName() === tagName) {
|
|
99
107
|
const file = imp.getModuleSpecifierSourceFile();
|
|
100
|
-
|
|
101
|
-
|
|
108
|
+
if (!file || file.getFilePath().includes('/node_modules/')) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const component = getComponent(file);
|
|
113
|
+
return component ?? null;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
102
118
|
}
|
|
103
119
|
}
|
|
104
120
|
}
|
|
105
121
|
return null;
|
|
106
122
|
};
|
|
123
|
+
const resolveLocalComponentFilePath = (node) => {
|
|
124
|
+
const tagName = getTagName(node);
|
|
125
|
+
const sourceFile = node.getSourceFile();
|
|
126
|
+
for (const imp of sourceFile.getImportDeclarations()) {
|
|
127
|
+
const matchesDefaultImport = imp.getDefaultImport()?.getText() === tagName;
|
|
128
|
+
const matchesNamedImport = imp
|
|
129
|
+
.getNamedImports()
|
|
130
|
+
.some((namedImport) => namedImport.getName() === tagName);
|
|
131
|
+
if (!matchesDefaultImport && !matchesNamedImport) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const moduleSpecifier = imp.getModuleSpecifierValue();
|
|
135
|
+
const isLocalImport = moduleSpecifier.startsWith('.') || moduleSpecifier.startsWith('/');
|
|
136
|
+
if (!isLocalImport) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
const importedSourceFile = imp.getModuleSpecifierSourceFile();
|
|
140
|
+
if (!importedSourceFile) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
const importedFilePath = importedSourceFile.getFilePath();
|
|
144
|
+
if (importedFilePath.includes('/node_modules/')) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
return importedFilePath;
|
|
148
|
+
}
|
|
149
|
+
// If there is no matching import, treat it as a component from the current local source file.
|
|
150
|
+
return sourceFile.getFilePath();
|
|
151
|
+
};
|
|
107
152
|
const buildNodeFromJSX = (node, project, options, depth) => {
|
|
108
153
|
const tagName = getTagName(node);
|
|
109
154
|
const isHtml = tagName[0] === tagName[0].toLowerCase();
|
|
@@ -113,7 +158,10 @@ const buildNodeFromJSX = (node, project, options, depth) => {
|
|
|
113
158
|
children: [],
|
|
114
159
|
};
|
|
115
160
|
if (!isHtml) {
|
|
116
|
-
|
|
161
|
+
const localComponentFilePath = resolveLocalComponentFilePath(node);
|
|
162
|
+
if (localComponentFilePath) {
|
|
163
|
+
treeNode.filePath = localComponentFilePath;
|
|
164
|
+
}
|
|
117
165
|
}
|
|
118
166
|
const props = extractPropsFromNode(node);
|
|
119
167
|
if (props) {
|
|
@@ -125,6 +173,10 @@ const buildNodeFromJSX = (node, project, options, depth) => {
|
|
|
125
173
|
}
|
|
126
174
|
}
|
|
127
175
|
}
|
|
176
|
+
const onClick = extractOnClickInfoFromNode(node);
|
|
177
|
+
if (onClick) {
|
|
178
|
+
treeNode.onClick = onClick;
|
|
179
|
+
}
|
|
128
180
|
// Recurse into children
|
|
129
181
|
if (Node.isJsxElement(node)) {
|
|
130
182
|
const children = node.getJsxChildren();
|
|
@@ -44,3 +44,62 @@ export const extractPropsFromNode = (node) => {
|
|
|
44
44
|
}
|
|
45
45
|
return props;
|
|
46
46
|
};
|
|
47
|
+
const getExpressionKind = (expression) => {
|
|
48
|
+
if (Node.isIdentifier(expression)) {
|
|
49
|
+
return 'identifier';
|
|
50
|
+
}
|
|
51
|
+
if (Node.isPropertyAccessExpression(expression)) {
|
|
52
|
+
return 'member-expression';
|
|
53
|
+
}
|
|
54
|
+
if (Node.isCallExpression(expression)) {
|
|
55
|
+
return 'call-expression';
|
|
56
|
+
}
|
|
57
|
+
if (Node.isArrowFunction(expression)) {
|
|
58
|
+
return 'arrow-function';
|
|
59
|
+
}
|
|
60
|
+
if (Node.isFunctionExpression(expression)) {
|
|
61
|
+
return 'function-expression';
|
|
62
|
+
}
|
|
63
|
+
return 'expression';
|
|
64
|
+
};
|
|
65
|
+
export const extractOnClickInfoFromNode = (node) => {
|
|
66
|
+
const attributes = Node.isJsxElement(node)
|
|
67
|
+
? node.getOpeningElement().getAttributes()
|
|
68
|
+
: node.getAttributes();
|
|
69
|
+
const onClickAttribute = attributes.find((attribute) => Node.isJsxAttribute(attribute) &&
|
|
70
|
+
attribute.getNameNode().getText() === 'onClick');
|
|
71
|
+
if (!onClickAttribute || !Node.isJsxAttribute(onClickAttribute)) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const initializer = onClickAttribute.getInitializer();
|
|
75
|
+
if (!initializer) {
|
|
76
|
+
return {
|
|
77
|
+
attribute: 'onClick',
|
|
78
|
+
expression: 'true',
|
|
79
|
+
kind: 'boolean-shorthand',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (Node.isStringLiteral(initializer)) {
|
|
83
|
+
return {
|
|
84
|
+
attribute: 'onClick',
|
|
85
|
+
expression: initializer.getLiteralText(),
|
|
86
|
+
kind: 'string-literal',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (Node.isJsxExpression(initializer)) {
|
|
90
|
+
const expression = initializer.getExpression();
|
|
91
|
+
if (!expression) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
attribute: 'onClick',
|
|
96
|
+
expression: expression.getText(),
|
|
97
|
+
kind: getExpressionKind(expression),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
attribute: 'onClick',
|
|
102
|
+
expression: initializer.getText(),
|
|
103
|
+
kind: 'expression',
|
|
104
|
+
};
|
|
105
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsc-typescript-ast-mcp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"mcpName": "io.github.jscoobyced/jsc-typescript-ast-mcp",
|
|
5
5
|
"description": "A Model Context Protocol (MCP) server that provides an abstract syntax tree (AST) representation of TypeScript code using the ts-morph library. It allows clients to analyze and manipulate TypeScript code structures, making it easier for AI models to understand and work with TypeScript projects. You can create a JSON representation of your React components, and use it for various purposes such as documentation, code analysis, or even generating new code based on existing components.",
|
|
6
6
|
"main": "dist/index.js",
|