project-graph-mcp 1.0.0
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/AGENT_ROLE.md +126 -0
- package/AGENT_ROLE_MINIMAL.md +54 -0
- package/CONFIGURATION.md +188 -0
- package/LICENSE +21 -0
- package/README.md +279 -0
- package/package.json +46 -0
- package/references/symbiote-3x.md +834 -0
- package/rules/express-5.json +76 -0
- package/rules/fastify-5.json +75 -0
- package/rules/nestjs-10.json +88 -0
- package/rules/nextjs-15.json +87 -0
- package/rules/node-22.json +156 -0
- package/rules/react-18.json +87 -0
- package/rules/react-19.json +76 -0
- package/rules/symbiote-2x.json +158 -0
- package/rules/symbiote-3x.json +221 -0
- package/rules/typescript-5.json +69 -0
- package/rules/vue-3.json +79 -0
- package/src/cli-handlers.js +140 -0
- package/src/cli.js +83 -0
- package/src/complexity.js +223 -0
- package/src/custom-rules.js +583 -0
- package/src/dead-code.js +468 -0
- package/src/filters.js +226 -0
- package/src/framework-references.js +177 -0
- package/src/full-analysis.js +159 -0
- package/src/graph-builder.js +269 -0
- package/src/instructions.js +175 -0
- package/src/jsdoc-generator.js +214 -0
- package/src/large-files.js +162 -0
- package/src/mcp-server.js +375 -0
- package/src/outdated-patterns.js +295 -0
- package/src/parser.js +293 -0
- package/src/server.js +28 -0
- package/src/similar-functions.js +278 -0
- package/src/test-annotations.js +301 -0
- package/src/tool-defs.js +444 -0
- package/src/tools.js +240 -0
- package/src/undocumented.js +260 -0
- package/src/workspace.js +70 -0
- package/vendor/acorn.mjs +6145 -0
- package/vendor/walk.mjs +437 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Guidelines and Instructions for AI Agents
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const AGENT_INSTRUCTIONS = `
|
|
6
|
+
# 🤖 Project Guidelines for AI Agents
|
|
7
|
+
|
|
8
|
+
## 1. Architecture Standards (Symbiote.js)
|
|
9
|
+
- **Component Structure**: Always use Triple-File Partitioning for components:
|
|
10
|
+
- \`MyComponent.js\`: Class logic (extends Symbiote)
|
|
11
|
+
- \`MyComponent.tpl.js\`: HTML template (export template)
|
|
12
|
+
- \`MyComponent.css.js\`: CSS styles (export rootStyles/shadowStyles)
|
|
13
|
+
- **State Management**: Use \`this.init$\` for local state and \`this.sub()\` for reactivity.
|
|
14
|
+
- **Directives**: Use \`itemize\` for lists, \`js-d-kit\` for static generation.
|
|
15
|
+
|
|
16
|
+
## 2. Test Annotations (@test/@expect)
|
|
17
|
+
Universal verification checklist system. Works for **any** test type.
|
|
18
|
+
|
|
19
|
+
### Syntax
|
|
20
|
+
\`\`\`javascript
|
|
21
|
+
/**
|
|
22
|
+
* Method description
|
|
23
|
+
*
|
|
24
|
+
* @test {type}: {description}
|
|
25
|
+
* @expect {type}: {description}
|
|
26
|
+
*/
|
|
27
|
+
async myMethod() { ... }
|
|
28
|
+
\`\`\`
|
|
29
|
+
|
|
30
|
+
### @test Types by Category
|
|
31
|
+
|
|
32
|
+
#### 🌐 Browser / UI
|
|
33
|
+
| Type | Description | Example |
|
|
34
|
+
|------|-------------|---------|
|
|
35
|
+
| \`click\` | Click element | \`@test click: Click submit button\` |
|
|
36
|
+
| \`key\` | Keyboard input | \`@test key: Press Enter\` |
|
|
37
|
+
| \`drag\` | Drag and drop | \`@test drag: Drag item to list\` |
|
|
38
|
+
| \`type\` | Text input | \`@test type: Enter email in field\` |
|
|
39
|
+
| \`scroll\` | Scroll action | \`@test scroll: Scroll to bottom\` |
|
|
40
|
+
| \`hover\` | Mouse hover | \`@test hover: Hover over menu\` |
|
|
41
|
+
|
|
42
|
+
#### 🔌 API / Function
|
|
43
|
+
| Type | Description | Example |
|
|
44
|
+
|------|-------------|---------|
|
|
45
|
+
| \`request\` | HTTP request | \`@test request: POST /api/users\` |
|
|
46
|
+
| \`call\` | Function call | \`@test call: Call with valid params\` |
|
|
47
|
+
| \`invoke\` | Method invoke | \`@test invoke: Trigger event\` |
|
|
48
|
+
| \`mock\` | Mock setup | \`@test mock: Mock external service\` |
|
|
49
|
+
|
|
50
|
+
#### 💻 CLI / Process
|
|
51
|
+
| Type | Description | Example |
|
|
52
|
+
|------|-------------|---------|
|
|
53
|
+
| \`run\` | Run command | \`@test run: Run with --help flag\` |
|
|
54
|
+
| \`exec\` | Execute script | \`@test exec: Execute build script\` |
|
|
55
|
+
| \`spawn\` | Spawn process | \`@test spawn: Start server\` |
|
|
56
|
+
| \`input\` | Stdin input | \`@test input: Enter password\` |
|
|
57
|
+
|
|
58
|
+
#### 🔗 Integration / System
|
|
59
|
+
| Type | Description | Example |
|
|
60
|
+
|------|-------------|---------|
|
|
61
|
+
| \`setup\` | Test setup | \`@test setup: Create test database\` |
|
|
62
|
+
| \`action\` | Main action | \`@test action: Run migration\` |
|
|
63
|
+
| \`teardown\` | Cleanup | \`@test teardown: Remove temp files\` |
|
|
64
|
+
| \`wait\` | Wait condition | \`@test wait: Wait for DB connection\` |
|
|
65
|
+
|
|
66
|
+
### @expect Types by Category
|
|
67
|
+
|
|
68
|
+
#### 🌐 Browser / UI
|
|
69
|
+
| Type | Description | Example |
|
|
70
|
+
|------|-------------|---------|
|
|
71
|
+
| \`attr\` | Attribute check | \`@expect attr: disabled attribute set\` |
|
|
72
|
+
| \`visual\` | Visual change | \`@expect visual: Button turns green\` |
|
|
73
|
+
| \`element\` | Element exists | \`@expect element: Modal appears\` |
|
|
74
|
+
| \`text\` | Text content | \`@expect text: Shows "Success"\` |
|
|
75
|
+
|
|
76
|
+
#### 🔌 API / Function
|
|
77
|
+
| Type | Description | Example |
|
|
78
|
+
|------|-------------|---------|
|
|
79
|
+
| \`status\` | HTTP status | \`@expect status: 201 Created\` |
|
|
80
|
+
| \`body\` | Response body | \`@expect body: Contains user ID\` |
|
|
81
|
+
| \`headers\` | Response headers | \`@expect headers: Content-Type JSON\` |
|
|
82
|
+
| \`error\` | Error thrown | \`@expect error: Throws ValidationError\` |
|
|
83
|
+
|
|
84
|
+
#### 💻 CLI / Process
|
|
85
|
+
| Type | Description | Example |
|
|
86
|
+
|------|-------------|---------|
|
|
87
|
+
| \`output\` | Stdout content | \`@expect output: Prints version\` |
|
|
88
|
+
| \`exitcode\` | Exit code | \`@expect exitcode: Returns 0\` |
|
|
89
|
+
| \`file\` | File created | \`@expect file: Creates config.json\` |
|
|
90
|
+
| \`stderr\` | Stderr content | \`@expect stderr: No errors\` |
|
|
91
|
+
|
|
92
|
+
#### 🔗 Integration / System
|
|
93
|
+
| Type | Description | Example |
|
|
94
|
+
|------|-------------|---------|
|
|
95
|
+
| \`state\` | State change | \`@expect state: User logged in\` |
|
|
96
|
+
| \`log\` | Log entry | \`@expect log: Info message logged\` |
|
|
97
|
+
| \`event\` | Event fired | \`@expect event: 'updated' emitted\` |
|
|
98
|
+
| \`db\` | Database change | \`@expect db: Row inserted\` |
|
|
99
|
+
|
|
100
|
+
### Full Example
|
|
101
|
+
\`\`\`javascript
|
|
102
|
+
/**
|
|
103
|
+
* Create new user via API
|
|
104
|
+
*
|
|
105
|
+
* @test request: POST /api/users with valid data
|
|
106
|
+
* @test call: Validate email format
|
|
107
|
+
*
|
|
108
|
+
* @expect status: 201 Created
|
|
109
|
+
* @expect body: Contains user ID and email
|
|
110
|
+
* @expect db: User row created in database
|
|
111
|
+
* @expect event: 'user.created' event emitted
|
|
112
|
+
*/
|
|
113
|
+
async createUser(data) {
|
|
114
|
+
// ...
|
|
115
|
+
}
|
|
116
|
+
\`\`\`
|
|
117
|
+
|
|
118
|
+
## 3. General Coding Rules
|
|
119
|
+
- **ESM Only**: Use \`import\` / \`export\`. No \`require\`.
|
|
120
|
+
- **No Dependencies**: Avoid adding new npm packages unless critical.
|
|
121
|
+
- **Comments**: Write clear JSDoc for all public methods.
|
|
122
|
+
- **Async/Await**: Prefer async/await over promises.
|
|
123
|
+
|
|
124
|
+
## 4. MCP Tools Usage
|
|
125
|
+
- **Graph**: Use \`get_skeleton\` first to map the codebase.
|
|
126
|
+
- **Deep Dive**: Use \`expand\` to read class details.
|
|
127
|
+
- **Tests**: Use \`get_pending_tests\` to see what needs verification.
|
|
128
|
+
- **Guidelines**: Use \`get_agent_instructions\` to refresh these rules.
|
|
129
|
+
|
|
130
|
+
## 5. Custom Rules System
|
|
131
|
+
Configurable code analysis with auto-detection.
|
|
132
|
+
|
|
133
|
+
### Available Tools
|
|
134
|
+
- \`get_custom_rules\`: List all rulesets and their rules
|
|
135
|
+
- \`set_custom_rule\`: Add or update a rule in a ruleset
|
|
136
|
+
- \`check_custom_rules\`: Run analysis (auto-detects applicable rulesets)
|
|
137
|
+
|
|
138
|
+
### Auto-Detection
|
|
139
|
+
Rulesets are applied automatically based on:
|
|
140
|
+
1. \`package.json\` dependencies
|
|
141
|
+
2. Import patterns in source code
|
|
142
|
+
3. Code patterns (e.g., \`extends Symbiote\`)
|
|
143
|
+
|
|
144
|
+
### Creating New Rules
|
|
145
|
+
Use \`set_custom_rule\` to add framework-specific rules:
|
|
146
|
+
\`\`\`json
|
|
147
|
+
{
|
|
148
|
+
"ruleSet": "my-framework-2x",
|
|
149
|
+
"rule": {
|
|
150
|
+
"id": "my-rule-id",
|
|
151
|
+
"name": "Rule Name",
|
|
152
|
+
"description": "What this rule checks",
|
|
153
|
+
"pattern": "badPattern",
|
|
154
|
+
"patternType": "string",
|
|
155
|
+
"replacement": "Use goodPattern instead",
|
|
156
|
+
"severity": "warning",
|
|
157
|
+
"filePattern": "*.js",
|
|
158
|
+
"docs": "https://docs.example.com/rule"
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
\`\`\`
|
|
162
|
+
|
|
163
|
+
### Severity Levels
|
|
164
|
+
- \`error\`: Critical issues that must be fixed
|
|
165
|
+
- \`warning\`: Important but not blocking
|
|
166
|
+
- \`info\`: Suggestions and best practices
|
|
167
|
+
`;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get agent instructions
|
|
171
|
+
* @returns {string}
|
|
172
|
+
*/
|
|
173
|
+
export function getInstructions() {
|
|
174
|
+
return AGENT_INSTRUCTIONS;
|
|
175
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSDoc Generator
|
|
3
|
+
* Auto-generates JSDoc templates from AST analysis
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from 'fs';
|
|
7
|
+
import { relative } from 'path';
|
|
8
|
+
import { parse } from '../vendor/acorn.mjs';
|
|
9
|
+
import * as walk from '../vendor/walk.mjs';
|
|
10
|
+
import { getWorkspaceRoot } from './workspace.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} JSDocTemplate
|
|
14
|
+
* @property {string} name - Function/method name
|
|
15
|
+
* @property {string} type - 'function' | 'method' | 'class'
|
|
16
|
+
* @property {string} file
|
|
17
|
+
* @property {number} line
|
|
18
|
+
* @property {string} jsdoc - Generated JSDoc template
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate JSDoc for a single file
|
|
23
|
+
* @param {string} filePath - Absolute path to file
|
|
24
|
+
* @param {Object} [options]
|
|
25
|
+
* @param {boolean} [options.includeTests=true] - Include @test/@expect placeholders
|
|
26
|
+
* @returns {JSDocTemplate[]}
|
|
27
|
+
*/
|
|
28
|
+
export function generateJSDoc(filePath, options = {}) {
|
|
29
|
+
const includeTests = options.includeTests !== false;
|
|
30
|
+
const results = [];
|
|
31
|
+
|
|
32
|
+
const code = readFileSync(filePath, 'utf-8');
|
|
33
|
+
const relPath = relative(getWorkspaceRoot(), filePath);
|
|
34
|
+
|
|
35
|
+
let ast;
|
|
36
|
+
try {
|
|
37
|
+
ast = parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
|
|
38
|
+
} catch (e) {
|
|
39
|
+
return results;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check if line already has JSDoc
|
|
43
|
+
const hasJSDocAt = (line) => {
|
|
44
|
+
const lines = code.split('\n');
|
|
45
|
+
// Look backwards from function line for JSDoc closing */
|
|
46
|
+
for (let i = line - 2; i >= Math.max(0, line - 15); i--) {
|
|
47
|
+
const trimmed = lines[i]?.trim();
|
|
48
|
+
if (!trimmed) continue; // Skip empty lines
|
|
49
|
+
// Found JSDoc end - look for start
|
|
50
|
+
if (trimmed === '*/' || trimmed.endsWith('*/')) {
|
|
51
|
+
// Now look for /** opening above
|
|
52
|
+
for (let j = i - 1; j >= Math.max(0, i - 20); j--) {
|
|
53
|
+
const upper = lines[j]?.trim();
|
|
54
|
+
if (upper?.startsWith('/**')) return true;
|
|
55
|
+
// If we hit something non-JSDoc, stop
|
|
56
|
+
if (upper && !upper.startsWith('*')) break;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
// If we hit code, stop
|
|
61
|
+
if (!trimmed.startsWith('*') && !trimmed.startsWith('//')) break;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
walk.simple(ast, {
|
|
67
|
+
FunctionDeclaration(node) {
|
|
68
|
+
if (!node.id) return;
|
|
69
|
+
if (hasJSDocAt(node.loc.start.line)) return;
|
|
70
|
+
|
|
71
|
+
const jsdoc = buildJSDoc({
|
|
72
|
+
name: node.id.name,
|
|
73
|
+
params: node.params,
|
|
74
|
+
async: node.async,
|
|
75
|
+
includeTests,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
results.push({
|
|
79
|
+
name: node.id.name,
|
|
80
|
+
type: 'function',
|
|
81
|
+
file: relPath,
|
|
82
|
+
line: node.loc.start.line,
|
|
83
|
+
jsdoc,
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
ClassDeclaration(node) {
|
|
88
|
+
if (!node.id) return;
|
|
89
|
+
|
|
90
|
+
// Check methods
|
|
91
|
+
for (const element of node.body.body) {
|
|
92
|
+
if (element.type === 'MethodDefinition') {
|
|
93
|
+
const methodName = element.key.name || element.key.value;
|
|
94
|
+
|
|
95
|
+
// Skip constructor, getters, setters, private
|
|
96
|
+
if (element.kind !== 'method') continue;
|
|
97
|
+
if (methodName.startsWith('_')) continue;
|
|
98
|
+
if (hasJSDocAt(element.loc.start.line)) continue;
|
|
99
|
+
|
|
100
|
+
const funcNode = element.value;
|
|
101
|
+
const jsdoc = buildJSDoc({
|
|
102
|
+
name: methodName,
|
|
103
|
+
params: funcNode.params,
|
|
104
|
+
async: funcNode.async,
|
|
105
|
+
includeTests,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
results.push({
|
|
109
|
+
name: `${node.id.name}.${methodName}`,
|
|
110
|
+
type: 'method',
|
|
111
|
+
file: relPath,
|
|
112
|
+
line: element.loc.start.line,
|
|
113
|
+
jsdoc,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return results;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Build JSDoc string from function info
|
|
125
|
+
* @param {Object} info
|
|
126
|
+
* @param {string} info.name
|
|
127
|
+
* @param {Array} info.params
|
|
128
|
+
* @param {boolean} info.async
|
|
129
|
+
* @param {boolean} info.includeTests
|
|
130
|
+
* @returns {string}
|
|
131
|
+
*/
|
|
132
|
+
function buildJSDoc(info) {
|
|
133
|
+
const lines = ['/**'];
|
|
134
|
+
|
|
135
|
+
// Description placeholder
|
|
136
|
+
lines.push(` * TODO: Add description for ${info.name}`);
|
|
137
|
+
|
|
138
|
+
// Parameters
|
|
139
|
+
for (const param of info.params) {
|
|
140
|
+
const paramName = extractParamName(param);
|
|
141
|
+
const paramType = inferParamType(param);
|
|
142
|
+
lines.push(` * @param {${paramType}} ${paramName}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Return type
|
|
146
|
+
lines.push(` * @returns {${info.async ? 'Promise<*>' : '*'}}`);
|
|
147
|
+
|
|
148
|
+
// Test annotations (Agentic Verification)
|
|
149
|
+
if (info.includeTests) {
|
|
150
|
+
lines.push(` * @test TODO: describe test scenario`);
|
|
151
|
+
lines.push(` * @expect TODO: expected result`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
lines.push(' */');
|
|
155
|
+
return lines.join('\n');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Extract parameter name from AST node
|
|
160
|
+
* @param {Object} param
|
|
161
|
+
* @returns {string}
|
|
162
|
+
*/
|
|
163
|
+
function extractParamName(param) {
|
|
164
|
+
if (param.type === 'Identifier') {
|
|
165
|
+
return param.name;
|
|
166
|
+
}
|
|
167
|
+
if (param.type === 'AssignmentPattern' && param.left.type === 'Identifier') {
|
|
168
|
+
return `[${param.left.name}]`; // Optional param
|
|
169
|
+
}
|
|
170
|
+
if (param.type === 'RestElement' && param.argument.type === 'Identifier') {
|
|
171
|
+
return `...${param.argument.name}`;
|
|
172
|
+
}
|
|
173
|
+
if (param.type === 'ObjectPattern') {
|
|
174
|
+
return 'options';
|
|
175
|
+
}
|
|
176
|
+
if (param.type === 'ArrayPattern') {
|
|
177
|
+
return 'args';
|
|
178
|
+
}
|
|
179
|
+
return 'param';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Infer parameter type from AST
|
|
184
|
+
* @param {Object} param
|
|
185
|
+
* @returns {string}
|
|
186
|
+
*/
|
|
187
|
+
function inferParamType(param) {
|
|
188
|
+
if (param.type === 'AssignmentPattern') {
|
|
189
|
+
const defaultVal = param.right;
|
|
190
|
+
if (defaultVal.type === 'Literal') {
|
|
191
|
+
if (typeof defaultVal.value === 'string') return 'string';
|
|
192
|
+
if (typeof defaultVal.value === 'number') return 'number';
|
|
193
|
+
if (typeof defaultVal.value === 'boolean') return 'boolean';
|
|
194
|
+
}
|
|
195
|
+
if (defaultVal.type === 'ArrayExpression') return 'Array';
|
|
196
|
+
if (defaultVal.type === 'ObjectExpression') return 'Object';
|
|
197
|
+
}
|
|
198
|
+
if (param.type === 'RestElement') return 'Array';
|
|
199
|
+
if (param.type === 'ObjectPattern') return 'Object';
|
|
200
|
+
if (param.type === 'ArrayPattern') return 'Array';
|
|
201
|
+
return '*';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Generate JSDoc for specific function by name
|
|
206
|
+
* @param {string} filePath
|
|
207
|
+
* @param {string} functionName
|
|
208
|
+
* @param {Object} [options]
|
|
209
|
+
* @returns {JSDocTemplate|null}
|
|
210
|
+
*/
|
|
211
|
+
export function generateJSDocFor(filePath, functionName, options = {}) {
|
|
212
|
+
const results = generateJSDoc(filePath, options);
|
|
213
|
+
return results.find(r => r.name === functionName || r.name.endsWith(`.${functionName}`)) || null;
|
|
214
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Large Files Analyzer
|
|
3
|
+
* Identifies files that may need splitting
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
7
|
+
import { join, relative, resolve } from 'path';
|
|
8
|
+
import { parse } from '../vendor/acorn.mjs';
|
|
9
|
+
import * as walk from '../vendor/walk.mjs';
|
|
10
|
+
import { shouldExcludeDir, shouldExcludeFile, parseGitignore } from './filters.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} LargeFileItem
|
|
14
|
+
* @property {string} file
|
|
15
|
+
* @property {number} lines
|
|
16
|
+
* @property {number} functions
|
|
17
|
+
* @property {number} classes
|
|
18
|
+
* @property {number} exports
|
|
19
|
+
* @property {string} rating - 'ok' | 'warning' | 'critical'
|
|
20
|
+
* @property {string[]} reasons
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Find all JS files
|
|
25
|
+
* @param {string} dir
|
|
26
|
+
* @param {string} rootDir
|
|
27
|
+
* @returns {string[]}
|
|
28
|
+
*/
|
|
29
|
+
function findJSFiles(dir, rootDir = dir) {
|
|
30
|
+
if (dir === rootDir) parseGitignore(rootDir);
|
|
31
|
+
const files = [];
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
for (const entry of readdirSync(dir)) {
|
|
35
|
+
const fullPath = join(dir, entry);
|
|
36
|
+
const relativePath = relative(rootDir, fullPath);
|
|
37
|
+
const stat = statSync(fullPath);
|
|
38
|
+
|
|
39
|
+
if (stat.isDirectory()) {
|
|
40
|
+
if (!shouldExcludeDir(entry, relativePath)) {
|
|
41
|
+
files.push(...findJSFiles(fullPath, rootDir));
|
|
42
|
+
}
|
|
43
|
+
} else if (entry.endsWith('.js') && !entry.endsWith('.css.js') && !entry.endsWith('.tpl.js')) {
|
|
44
|
+
if (!shouldExcludeFile(entry, relativePath)) {
|
|
45
|
+
files.push(fullPath);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch (e) { }
|
|
50
|
+
|
|
51
|
+
return files;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Analyze a single file
|
|
56
|
+
* @param {string} filePath
|
|
57
|
+
* @returns {LargeFileItem}
|
|
58
|
+
*/
|
|
59
|
+
function analyzeFile(filePath, rootDir) {
|
|
60
|
+
const code = readFileSync(filePath, 'utf-8');
|
|
61
|
+
const relPath = relative(rootDir, filePath);
|
|
62
|
+
const lines = code.split('\n').length;
|
|
63
|
+
|
|
64
|
+
let functions = 0;
|
|
65
|
+
let classes = 0;
|
|
66
|
+
let exports = 0;
|
|
67
|
+
|
|
68
|
+
let ast;
|
|
69
|
+
try {
|
|
70
|
+
ast = parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return { file: relPath, lines, functions: 0, classes: 0, exports: 0, rating: 'ok', reasons: [] };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
walk.simple(ast, {
|
|
76
|
+
FunctionDeclaration() { functions++; },
|
|
77
|
+
ArrowFunctionExpression(node) {
|
|
78
|
+
if (node.body.type === 'BlockStatement') functions++;
|
|
79
|
+
},
|
|
80
|
+
ClassDeclaration() { classes++; },
|
|
81
|
+
ExportNamedDeclaration() { exports++; },
|
|
82
|
+
ExportDefaultDeclaration() { exports++; },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Calculate rating
|
|
86
|
+
const reasons = [];
|
|
87
|
+
let score = 0;
|
|
88
|
+
|
|
89
|
+
if (lines > 500) {
|
|
90
|
+
score += 2;
|
|
91
|
+
reasons.push(`${lines} lines (>500)`);
|
|
92
|
+
} else if (lines > 300) {
|
|
93
|
+
score += 1;
|
|
94
|
+
reasons.push(`${lines} lines (>300)`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (functions > 15) {
|
|
98
|
+
score += 2;
|
|
99
|
+
reasons.push(`${functions} functions (>15)`);
|
|
100
|
+
} else if (functions > 10) {
|
|
101
|
+
score += 1;
|
|
102
|
+
reasons.push(`${functions} functions (>10)`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (classes > 3) {
|
|
106
|
+
score += 2;
|
|
107
|
+
reasons.push(`${classes} classes (>3)`);
|
|
108
|
+
} else if (classes > 1) {
|
|
109
|
+
score += 1;
|
|
110
|
+
reasons.push(`${classes} classes (>1)`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (exports > 10) {
|
|
114
|
+
score += 2;
|
|
115
|
+
reasons.push(`${exports} exports (>10)`);
|
|
116
|
+
} else if (exports > 5) {
|
|
117
|
+
score += 1;
|
|
118
|
+
reasons.push(`${exports} exports (>5)`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let rating = 'ok';
|
|
122
|
+
if (score >= 4) rating = 'critical';
|
|
123
|
+
else if (score >= 2) rating = 'warning';
|
|
124
|
+
|
|
125
|
+
return { file: relPath, lines, functions, classes, exports, rating, reasons };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get large files analysis
|
|
130
|
+
* @param {string} dir
|
|
131
|
+
* @param {Object} [options]
|
|
132
|
+
* @param {boolean} [options.onlyProblematic=false] - Only show warning/critical
|
|
133
|
+
* @returns {Promise<{total: number, stats: Object, items: LargeFileItem[]}>}
|
|
134
|
+
*/
|
|
135
|
+
export async function getLargeFiles(dir, options = {}) {
|
|
136
|
+
const onlyProblematic = options.onlyProblematic || false;
|
|
137
|
+
const resolvedDir = resolve(dir);
|
|
138
|
+
const files = findJSFiles(dir);
|
|
139
|
+
let items = files.map(f => analyzeFile(f, resolvedDir));
|
|
140
|
+
|
|
141
|
+
if (onlyProblematic) {
|
|
142
|
+
items = items.filter(i => i.rating !== 'ok');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Sort by lines descending
|
|
146
|
+
items.sort((a, b) => b.lines - a.lines);
|
|
147
|
+
|
|
148
|
+
const stats = {
|
|
149
|
+
totalFiles: files.length,
|
|
150
|
+
ok: items.filter(i => i.rating === 'ok').length,
|
|
151
|
+
warning: items.filter(i => i.rating === 'warning').length,
|
|
152
|
+
critical: items.filter(i => i.rating === 'critical').length,
|
|
153
|
+
totalLines: items.reduce((s, i) => s + i.lines, 0),
|
|
154
|
+
avgLines: items.length > 0 ? Math.round(items.reduce((s, i) => s + i.lines, 0) / items.length) : 0,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
total: items.length,
|
|
159
|
+
stats,
|
|
160
|
+
items: items.slice(0, 30),
|
|
161
|
+
};
|
|
162
|
+
}
|