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
- import { getComponentName } from './nameHelper.js';
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 component = sourceFile.getDefaultExportSymbol()?.getDeclarations()?.[0];
5
- if (!component) {
6
- throw new Error(`Component not found in ${sourceFile.getFilePath()}`);
7
- }
8
- const componentName = getComponentName(component);
9
- if (componentName !== 'AnonymousComponent')
10
- return component;
11
- const components = sourceFile.getVariableDeclarations().filter((v) => {
12
- const initializer = v.getInitializer();
13
- return initializer?.getKind() === SyntaxKind.ArrowFunction;
14
- });
15
- return components[0];
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
- const component = getComponent(file);
95
- return component ?? null;
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
- const component = getComponent(file);
101
- return component ?? null;
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
- treeNode.filePath = node.getSourceFile().getFilePath();
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.0",
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",