reactjsquality-check911 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/bin/reactjsquality-check911.js +41 -0
- package/commands/coverage.js +117 -0
- package/commands/hooks.js +74 -0
- package/commands/init.js +227 -0
- package/commands/playwright.js +52 -0
- package/commands/quality.js +158 -0
- package/commands/scan.js +243 -0
- package/mcp-server/requirements.txt +1 -0
- package/mcp-server/server.py +61 -0
- package/mcp-server/tools/code_fixer.py +130 -0
- package/mcp-server/tools/component_scanner.py +47 -0
- package/mcp-server/tools/playwright_helper.py +78 -0
- package/mcp-server/tools/test_generator.py +119 -0
- package/package.json +44 -0
- package/scripts/setup-mcp.js +18 -0
package/commands/scan.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
function readFile(f) { try { return fs.readFileSync(f, "utf8"); } catch (_) { return ""; } }
|
|
7
|
+
|
|
8
|
+
const SRC_EXTS = /\.(js|jsx|ts|tsx|vue|svelte)$/;
|
|
9
|
+
|
|
10
|
+
// ── file walkers ──────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
function walk(dir, buckets) {
|
|
13
|
+
if (!fs.existsSync(dir)) return;
|
|
14
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
15
|
+
const full = path.join(dir, entry.name);
|
|
16
|
+
if (entry.isDirectory()) {
|
|
17
|
+
if (/node_modules|\.git|dist|build|\.next|coverage|\.cache/.test(entry.name)) continue;
|
|
18
|
+
walk(full, buckets);
|
|
19
|
+
} else if (SRC_EXTS.test(entry.name)) {
|
|
20
|
+
classify(full, entry.name, buckets);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function classify(full, name, buckets) {
|
|
26
|
+
if (name.includes(".test.") || name.includes(".spec.") || full.includes("__tests__")) {
|
|
27
|
+
buckets.tests.push(full); return;
|
|
28
|
+
}
|
|
29
|
+
const src = readFile(full);
|
|
30
|
+
if (/\bplaywright\b|test\.describe|test\.it|test\(/.test(src) && !src.includes("export default")) {
|
|
31
|
+
buckets.e2e.push(full); return;
|
|
32
|
+
}
|
|
33
|
+
if (/pages\/|app\/.*page\.(js|jsx|ts|tsx)$|routes\//.test(full.replace(/\\/g, "/"))) {
|
|
34
|
+
buckets.pages.push(full); return;
|
|
35
|
+
}
|
|
36
|
+
if (/hooks\/|use[A-Z]\w+\.(js|jsx|ts|tsx)$/.test(full.replace(/\\/g, "/"))) {
|
|
37
|
+
buckets.hooks.push(full); return;
|
|
38
|
+
}
|
|
39
|
+
if (/context\/|provider\/|Context\.(js|jsx|ts|tsx)$|Provider\.(js|jsx|ts|tsx)$/.test(full.replace(/\\/g, "/"))) {
|
|
40
|
+
buckets.contexts.push(full); return;
|
|
41
|
+
}
|
|
42
|
+
if (/services\/|api\/|client\/|\.service\.(js|ts)$/.test(full.replace(/\\/g, "/"))) {
|
|
43
|
+
buckets.services.push(full); return;
|
|
44
|
+
}
|
|
45
|
+
if (/store\/|slice\/|reducer\/|\.slice\.(js|ts)$|\.store\.(js|ts)$/.test(full.replace(/\\/g, "/"))) {
|
|
46
|
+
buckets.stores.push(full); return;
|
|
47
|
+
}
|
|
48
|
+
if (/utils\/|helpers\/|lib\/|\.util\.(js|ts)$|\.helper\.(js|ts)$/.test(full.replace(/\\/g, "/"))) {
|
|
49
|
+
buckets.utils.push(full); return;
|
|
50
|
+
}
|
|
51
|
+
if (src.includes("export default") && (src.includes("return (") || src.includes("=> ("))) {
|
|
52
|
+
buckets.components.push(full); return;
|
|
53
|
+
}
|
|
54
|
+
buckets.other.push(full);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── component analysis ────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
function extractComponentName(src, filePath) {
|
|
60
|
+
const m = /(?:export default function|const|function)\s+([A-Z]\w+)/.exec(src);
|
|
61
|
+
return m ? m[1] : path.basename(filePath, path.extname(filePath));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function extractProps(src) {
|
|
65
|
+
const m = /(?:interface|type)\s+\w*Props\s*[={]([^}]+)}/s.exec(src);
|
|
66
|
+
if (!m) return [];
|
|
67
|
+
return (m[1].match(/(\w+)\s*[?:]?\s*:/g) || []).map(p => p.replace(/[?:]/g, "").trim()).slice(0, 8);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function extractHookDeps(src) {
|
|
71
|
+
const deps = new Set();
|
|
72
|
+
const re = /use([A-Z]\w+)\s*\(/g;
|
|
73
|
+
let m;
|
|
74
|
+
while ((m = re.exec(src)) !== null) {
|
|
75
|
+
const hook = "use" + m[1];
|
|
76
|
+
if (!["useState", "useEffect", "useCallback", "useMemo", "useRef", "useContext", "useReducer"].includes(hook))
|
|
77
|
+
deps.add(hook);
|
|
78
|
+
}
|
|
79
|
+
return [...deps].slice(0, 5);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function extractApiCalls(src) {
|
|
83
|
+
const calls = [];
|
|
84
|
+
const re = /(?:fetch|axios\.(?:get|post|put|delete|patch))\s*\(\s*['"`]([^'"`]+)['"`]/g;
|
|
85
|
+
let m;
|
|
86
|
+
while ((m = re.exec(src)) !== null) calls.push(m[1]);
|
|
87
|
+
return [...new Set(calls)].slice(0, 5);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function extractRoutes(src, filePath) {
|
|
91
|
+
const fp = filePath.replace(/\\/g, "/");
|
|
92
|
+
const routeM = /pages\/(.+?)\.(js|jsx|ts|tsx)$/.exec(fp) || /app\/(.+?)\/page\.(js|jsx|ts|tsx)$/.exec(fp);
|
|
93
|
+
return routeM ? "/" + routeM[1].replace(/\/index$/, "").replace(/\[(\w+)\]/, ":$1") : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── markdown builders ─────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
function buildComponentIndex(buckets) {
|
|
99
|
+
const rel = f => f.replace(/\\/g, "/");
|
|
100
|
+
let md = "# Component Index\n\n";
|
|
101
|
+
|
|
102
|
+
const sections = [
|
|
103
|
+
["Pages / Routes", buckets.pages],
|
|
104
|
+
["Components", buckets.components],
|
|
105
|
+
["Custom Hooks", buckets.hooks],
|
|
106
|
+
["State / Stores", buckets.stores],
|
|
107
|
+
["Contexts", buckets.contexts],
|
|
108
|
+
["Services / API", buckets.services],
|
|
109
|
+
["Utilities", buckets.utils],
|
|
110
|
+
["Tests (Unit)", buckets.tests],
|
|
111
|
+
["Tests (E2E)", buckets.e2e],
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
for (const [title, files] of sections) {
|
|
115
|
+
if (!files.length) continue;
|
|
116
|
+
md += `## ${title}\n`;
|
|
117
|
+
files.forEach(f => (md += `- [${path.basename(f)}](${rel(f)})\n`));
|
|
118
|
+
md += "\n";
|
|
119
|
+
}
|
|
120
|
+
return md;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function buildArchitecture(buckets) {
|
|
124
|
+
let md = "# Frontend Architecture\n\n";
|
|
125
|
+
|
|
126
|
+
// Overview
|
|
127
|
+
md += "## Overview\n\n";
|
|
128
|
+
md += `| | |\n|---|---|\n`;
|
|
129
|
+
md += `| **Pages / Routes** | ${buckets.pages.length} |\n`;
|
|
130
|
+
md += `| **Components** | ${buckets.components.length} |\n`;
|
|
131
|
+
md += `| **Custom Hooks** | ${buckets.hooks.length} |\n`;
|
|
132
|
+
md += `| **State / Stores** | ${buckets.stores.length} |\n`;
|
|
133
|
+
md += `| **Services / API** | ${buckets.services.length} |\n`;
|
|
134
|
+
md += `| **Unit Tests** | ${buckets.tests.length} |\n`;
|
|
135
|
+
md += `| **E2E Tests** | ${buckets.e2e.length} |\n\n`;
|
|
136
|
+
|
|
137
|
+
// Pages
|
|
138
|
+
if (buckets.pages.length) {
|
|
139
|
+
md += "---\n\n## Pages / Routes\n\n";
|
|
140
|
+
for (const f of buckets.pages) {
|
|
141
|
+
const src = readFile(f);
|
|
142
|
+
const name = extractComponentName(src, f);
|
|
143
|
+
const route = extractRoutes(src, f);
|
|
144
|
+
const apis = extractApiCalls(src);
|
|
145
|
+
md += `### ${name}\n\n`;
|
|
146
|
+
if (route) md += `**Route**: \`${route}\` \n`;
|
|
147
|
+
md += `**File**: \`${f.replace(/\\/g, "/")}\` \n`;
|
|
148
|
+
if (apis.length) md += `**API calls**: ${apis.map(a => `\`${a}\``).join(", ")} \n`;
|
|
149
|
+
md += "\n";
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Components
|
|
154
|
+
if (buckets.components.length) {
|
|
155
|
+
md += "---\n\n## Components\n\n";
|
|
156
|
+
for (const f of buckets.components) {
|
|
157
|
+
const src = readFile(f);
|
|
158
|
+
const name = extractComponentName(src, f);
|
|
159
|
+
const props = extractProps(src);
|
|
160
|
+
const hooks = extractHookDeps(src);
|
|
161
|
+
const apis = extractApiCalls(src);
|
|
162
|
+
|
|
163
|
+
md += `### ${name}\n\n`;
|
|
164
|
+
md += `**File**: \`${f.replace(/\\/g, "/")}\` \n`;
|
|
165
|
+
if (props.length) md += `**Props**: ${props.map(p => `\`${p}\``).join(", ")} \n`;
|
|
166
|
+
if (hooks.length) md += `**Uses hooks**: ${hooks.join(", ")} \n`;
|
|
167
|
+
if (apis.length) md += `**Fetches**: ${apis.map(a => `\`${a}\``).join(", ")} \n`;
|
|
168
|
+
md += "\n";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Custom Hooks
|
|
173
|
+
if (buckets.hooks.length) {
|
|
174
|
+
md += "---\n\n## Custom Hooks\n\n";
|
|
175
|
+
for (const f of buckets.hooks) {
|
|
176
|
+
const src = readFile(f);
|
|
177
|
+
const name = path.basename(f, path.extname(f));
|
|
178
|
+
const apis = extractApiCalls(src);
|
|
179
|
+
md += `### ${name}\n\n`;
|
|
180
|
+
md += `**File**: \`${f.replace(/\\/g, "/")}\` \n`;
|
|
181
|
+
if (apis.length) md += `**API calls**: ${apis.map(a => `\`${a}\``).join(", ")} \n`;
|
|
182
|
+
md += "\n";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Services
|
|
187
|
+
if (buckets.services.length) {
|
|
188
|
+
md += "---\n\n## Services / API Layer\n\n";
|
|
189
|
+
for (const f of buckets.services) {
|
|
190
|
+
const src = readFile(f);
|
|
191
|
+
const name = path.basename(f, path.extname(f));
|
|
192
|
+
const apis = extractApiCalls(src);
|
|
193
|
+
const fns = [...(src.matchAll(/export (?:async )?function (\w+)|export const (\w+)\s*=/g) || [])]
|
|
194
|
+
.map(m => m[1] || m[2]).filter(Boolean).slice(0, 8);
|
|
195
|
+
md += `### ${name}\n\n`;
|
|
196
|
+
md += `**File**: \`${f.replace(/\\/g, "/")}\` \n`;
|
|
197
|
+
if (fns.length) md += `**Exports**: ${fns.map(n => `\`${n}()\``).join(", ")} \n`;
|
|
198
|
+
if (apis.length) md += `**Endpoints**: ${apis.map(a => `\`${a}\``).join(", ")} \n`;
|
|
199
|
+
md += "\n";
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Stores
|
|
204
|
+
if (buckets.stores.length) {
|
|
205
|
+
md += "---\n\n## State / Stores\n\n";
|
|
206
|
+
for (const f of buckets.stores) {
|
|
207
|
+
const name = path.basename(f, path.extname(f));
|
|
208
|
+
md += `### ${name}\n\n`;
|
|
209
|
+
md += `**File**: \`${f.replace(/\\/g, "/")}\` \n\n`;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Layer diagram
|
|
214
|
+
md += "---\n\n## Application Layers\n\n```\n";
|
|
215
|
+
md += "Browser / User\n │\n ▼\n";
|
|
216
|
+
if (buckets.pages.length) md += "Pages / Routes ← Next.js routing or React Router\n │\n ▼\n";
|
|
217
|
+
if (buckets.components.length) md += "Components ← UI building blocks\n │\n ▼\n";
|
|
218
|
+
if (buckets.hooks.length) md += "Custom Hooks ← shared stateful logic\n │\n ▼\n";
|
|
219
|
+
if (buckets.stores.length) md += "State / Stores ← global state (Redux / Zustand / Pinia)\n │\n ▼\n";
|
|
220
|
+
if (buckets.services.length) md += "Services / API Layer ← HTTP calls to backend\n │\n ▼\n";
|
|
221
|
+
md += "Backend API\n```\n";
|
|
222
|
+
|
|
223
|
+
return md;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = function scan() {
|
|
227
|
+
const buckets = {
|
|
228
|
+
pages: [], components: [], hooks: [], contexts: [],
|
|
229
|
+
stores: [], services: [], utils: [], tests: [], e2e: [], other: []
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
console.log("Scanning repository...");
|
|
233
|
+
const srcDirs = ["src", "app", "pages", "components", "hooks", "services", "stores", "lib"];
|
|
234
|
+
for (const dir of srcDirs) walk(dir, buckets);
|
|
235
|
+
|
|
236
|
+
fs.mkdirSync("docs", { recursive: true });
|
|
237
|
+
|
|
238
|
+
fs.writeFileSync("docs/component-index.md", buildComponentIndex(buckets));
|
|
239
|
+
console.log("docs/component-index.md updated");
|
|
240
|
+
|
|
241
|
+
fs.writeFileSync("docs/architecture.md", buildArchitecture(buckets));
|
|
242
|
+
console.log("docs/architecture.md updated");
|
|
243
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fastmcp
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from fastmcp import FastMCP
|
|
3
|
+
from tools.code_fixer import code_fixer
|
|
4
|
+
from tools.test_generator import test_generator
|
|
5
|
+
from tools.component_scanner import component_scanner
|
|
6
|
+
from tools.playwright_helper import playwright_helper
|
|
7
|
+
|
|
8
|
+
mcp = FastMCP("ReactJS Quality Agent")
|
|
9
|
+
|
|
10
|
+
@mcp.tool()
|
|
11
|
+
def ping():
|
|
12
|
+
return "MCP Working"
|
|
13
|
+
|
|
14
|
+
@mcp.tool()
|
|
15
|
+
def fix():
|
|
16
|
+
"""
|
|
17
|
+
Get everything needed to fix staged files in one pass:
|
|
18
|
+
file contents + ESLint violations + fix instructions.
|
|
19
|
+
Call this when the user asks to fix quality issues in staged files.
|
|
20
|
+
"""
|
|
21
|
+
return code_fixer()
|
|
22
|
+
|
|
23
|
+
@mcp.tool()
|
|
24
|
+
def generate_tests():
|
|
25
|
+
"""
|
|
26
|
+
Generate Jest tests for staged or recently changed source files.
|
|
27
|
+
Call this when the user asks to generate tests, add test coverage,
|
|
28
|
+
or write unit tests.
|
|
29
|
+
"""
|
|
30
|
+
return test_generator()
|
|
31
|
+
|
|
32
|
+
@mcp.tool()
|
|
33
|
+
def scan():
|
|
34
|
+
"""
|
|
35
|
+
Scan the project and return a structured index of components, pages,
|
|
36
|
+
hooks, services, and stores. Use this first to navigate the codebase.
|
|
37
|
+
"""
|
|
38
|
+
return component_scanner()
|
|
39
|
+
|
|
40
|
+
@mcp.tool()
|
|
41
|
+
def playwright_setup():
|
|
42
|
+
"""
|
|
43
|
+
Return Playwright test generation instructions and existing test patterns
|
|
44
|
+
for the current project. Use this when the user asks to generate
|
|
45
|
+
Playwright / E2E tests for a page or user flow.
|
|
46
|
+
"""
|
|
47
|
+
return playwright_helper()
|
|
48
|
+
|
|
49
|
+
@mcp.tool()
|
|
50
|
+
def read_file(path: str) -> str:
|
|
51
|
+
"""
|
|
52
|
+
Read a specific source file by path.
|
|
53
|
+
Use scan() first to find the path, then call this.
|
|
54
|
+
"""
|
|
55
|
+
if not os.path.exists(path):
|
|
56
|
+
return f"File not found: {path}"
|
|
57
|
+
with open(path, encoding="utf-8") as f:
|
|
58
|
+
return f.read()
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
mcp.run()
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
SRC_EXTS = (".js", ".jsx", ".ts", ".tsx", ".vue", ".svelte")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_staged_files():
|
|
11
|
+
r = subprocess.run(["git", "diff", "--cached", "--name-only"],
|
|
12
|
+
capture_output=True, text=True)
|
|
13
|
+
return [f.strip() for f in r.stdout.splitlines()
|
|
14
|
+
if f.strip().endswith(SRC_EXTS) and os.path.exists(f.strip())]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_changed_line_ranges() -> dict:
|
|
18
|
+
r = subprocess.run(["git", "diff", "--cached", "--unified=0"],
|
|
19
|
+
capture_output=True, text=True)
|
|
20
|
+
ranges: dict = {}
|
|
21
|
+
cur = None
|
|
22
|
+
for line in r.stdout.splitlines():
|
|
23
|
+
fm = re.match(r'^\+\+\+ b/(.+)$', line)
|
|
24
|
+
if fm:
|
|
25
|
+
cur = fm.group(1)
|
|
26
|
+
ranges.setdefault(cur, [])
|
|
27
|
+
continue
|
|
28
|
+
if line.startswith("+++ /dev/null"):
|
|
29
|
+
cur = None
|
|
30
|
+
continue
|
|
31
|
+
hm = re.match(r'^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@', line)
|
|
32
|
+
if hm and cur:
|
|
33
|
+
start = int(hm.group(1))
|
|
34
|
+
count = int(hm.group(2)) if hm.group(2) is not None else 1
|
|
35
|
+
if count > 0:
|
|
36
|
+
ranges[cur].append([start, start + count - 1])
|
|
37
|
+
return ranges
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _find_ranges(abs_path: str, changed_ranges: dict):
|
|
41
|
+
norm = abs_path.replace("\\", "/").lower()
|
|
42
|
+
for git_path, ranges in changed_ranges.items():
|
|
43
|
+
gn = git_path.lower()
|
|
44
|
+
if norm == gn or norm.endswith("/" + gn):
|
|
45
|
+
return ranges
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _in_range(line_num: int, ranges) -> bool:
|
|
50
|
+
if not ranges:
|
|
51
|
+
return False
|
|
52
|
+
return any(s <= line_num <= e for s, e in ranges)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def run_eslint(files: list) -> list:
|
|
56
|
+
"""Run ESLint with JSON reporter and return raw results."""
|
|
57
|
+
try:
|
|
58
|
+
result = subprocess.run(
|
|
59
|
+
["npx", "eslint"] + files + ["--format", "json"],
|
|
60
|
+
capture_output=True, text=True
|
|
61
|
+
)
|
|
62
|
+
return json.loads(result.stdout or "[]")
|
|
63
|
+
except Exception:
|
|
64
|
+
return []
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def code_fixer() -> str:
|
|
68
|
+
staged = get_staged_files()
|
|
69
|
+
if not staged:
|
|
70
|
+
return "No staged JS/TS files found. Run `git add <files>` first."
|
|
71
|
+
|
|
72
|
+
changed_ranges = get_changed_line_ranges()
|
|
73
|
+
|
|
74
|
+
# Run ESLint
|
|
75
|
+
eslint_results = run_eslint(staged)
|
|
76
|
+
|
|
77
|
+
# Filter to changed chunks only
|
|
78
|
+
violations = []
|
|
79
|
+
for file_result in eslint_results:
|
|
80
|
+
fpath = file_result.get("filePath", "")
|
|
81
|
+
ranges = _find_ranges(fpath, changed_ranges)
|
|
82
|
+
if ranges is None:
|
|
83
|
+
continue
|
|
84
|
+
fname = os.path.basename(fpath)
|
|
85
|
+
for msg in file_result.get("messages", []):
|
|
86
|
+
if not _in_range(msg.get("line", 0), ranges):
|
|
87
|
+
continue
|
|
88
|
+
severity = "error" if msg.get("severity") == 2 else "warning"
|
|
89
|
+
rule = msg.get("ruleId") or "unknown"
|
|
90
|
+
line = msg.get("line", "?")
|
|
91
|
+
text = msg.get("message", "")
|
|
92
|
+
violations.append(f" [ESLint:{rule}] {fname}:{line} — {text} ({severity})")
|
|
93
|
+
|
|
94
|
+
if not violations:
|
|
95
|
+
return "No ESLint violations in your changed lines — nothing to fix."
|
|
96
|
+
|
|
97
|
+
# Read changed chunks only
|
|
98
|
+
chunks_text = ""
|
|
99
|
+
for f in staged:
|
|
100
|
+
git_path = f.replace("\\", "/")
|
|
101
|
+
file_ranges = changed_ranges.get(git_path) or next(
|
|
102
|
+
(rng for gp, rng in changed_ranges.items()
|
|
103
|
+
if gp.lower().endswith("/" + git_path.lower().split("/")[-1])),
|
|
104
|
+
None
|
|
105
|
+
)
|
|
106
|
+
lines = Path(f).read_text(encoding="utf-8").splitlines()
|
|
107
|
+
chunks = []
|
|
108
|
+
if file_ranges:
|
|
109
|
+
for start, end in file_ranges:
|
|
110
|
+
chunk_lines = lines[start - 1: end]
|
|
111
|
+
chunks.append(f"Lines {start}–{end}:\n" + "\n".join(chunk_lines))
|
|
112
|
+
chunks_text += f"\n### {f}\n" + "\n\n".join(chunks) + "\n"
|
|
113
|
+
|
|
114
|
+
violations_text = "\n".join(violations)
|
|
115
|
+
|
|
116
|
+
return f"""You are a Senior React/TypeScript Engineer. Fix ONLY the ESLint violations listed below.
|
|
117
|
+
They are all within the changed chunks of the current commit — do not touch any other lines.
|
|
118
|
+
|
|
119
|
+
## Changed Chunks
|
|
120
|
+
{chunks_text}
|
|
121
|
+
|
|
122
|
+
## ESLint Violations in Changed Lines
|
|
123
|
+
{violations_text}
|
|
124
|
+
|
|
125
|
+
## Instructions
|
|
126
|
+
1. Fix every ESLint error (unused variables, missing return types, console.log, any types)
|
|
127
|
+
2. Fix every TypeScript violation (implicit any, missing types, strict mode issues)
|
|
128
|
+
3. Change ONLY the lines reported above — do not reformat or refactor anything else
|
|
129
|
+
4. Return the corrected chunks showing the fixed lines with their line numbers
|
|
130
|
+
"""
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
SRC_EXTS = (".js", ".jsx", ".ts", ".tsx", ".vue", ".svelte")
|
|
6
|
+
SKIP_DIRS = {"node_modules", ".git", "dist", "build", ".next", "coverage", ".cache"}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def walk(root):
|
|
10
|
+
files = []
|
|
11
|
+
if not os.path.exists(root):
|
|
12
|
+
return files
|
|
13
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
14
|
+
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
|
15
|
+
for fname in filenames:
|
|
16
|
+
if fname.endswith(SRC_EXTS):
|
|
17
|
+
files.append(os.path.join(dirpath, fname))
|
|
18
|
+
return files
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def component_scanner() -> str:
|
|
22
|
+
src_dirs = ["src", "app", "pages", "components", "hooks", "services", "stores", "lib"]
|
|
23
|
+
all_files = []
|
|
24
|
+
for d in src_dirs:
|
|
25
|
+
all_files.extend(walk(d))
|
|
26
|
+
|
|
27
|
+
if not all_files:
|
|
28
|
+
return "No source files found. Run from the project root."
|
|
29
|
+
|
|
30
|
+
summary = []
|
|
31
|
+
for f in all_files[:60]: # cap to avoid huge responses
|
|
32
|
+
fp = f.replace("\\", "/")
|
|
33
|
+
name = os.path.basename(f)
|
|
34
|
+
try:
|
|
35
|
+
src = Path(f).read_text(encoding="utf-8")
|
|
36
|
+
except Exception:
|
|
37
|
+
continue
|
|
38
|
+
exports = re.findall(r'export\s+(?:default\s+)?(?:async\s+)?(?:function|const|class)\s+(\w+)', src)
|
|
39
|
+
api_calls = re.findall(r'(?:fetch|axios\.(?:get|post|put|delete|patch))\s*\(\s*[\'"`]([^\'"`]+)[\'"`]', src)
|
|
40
|
+
line = f"- {fp}"
|
|
41
|
+
if exports:
|
|
42
|
+
line += f" — exports: {', '.join(exports[:4])}"
|
|
43
|
+
if api_calls:
|
|
44
|
+
line += f" — calls: {', '.join(api_calls[:3])}"
|
|
45
|
+
summary.append(line)
|
|
46
|
+
|
|
47
|
+
return "## Project File Index\n\n" + "\n".join(summary)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_staged_files():
|
|
8
|
+
r = subprocess.run(["git", "diff", "--cached", "--name-only"],
|
|
9
|
+
capture_output=True, text=True)
|
|
10
|
+
return [f.strip() for f in r.stdout.splitlines()
|
|
11
|
+
if f.strip().endswith((".js", ".jsx", ".ts", ".tsx"))
|
|
12
|
+
and os.path.exists(f.strip())]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def find_existing_tests():
|
|
16
|
+
tests = []
|
|
17
|
+
for root, dirs, files in os.walk("."):
|
|
18
|
+
dirs[:] = [d for d in dirs if d not in {"node_modules", ".git", "dist", ".next"}]
|
|
19
|
+
for f in files:
|
|
20
|
+
if f.endswith(".spec.ts") or f.endswith(".spec.js"):
|
|
21
|
+
tests.append(os.path.join(root, f).replace("\\", "/"))
|
|
22
|
+
return tests[:5]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def playwright_helper() -> str:
|
|
26
|
+
staged = get_staged_files()
|
|
27
|
+
existing = find_existing_tests()
|
|
28
|
+
|
|
29
|
+
pages = [f for f in staged if "page" in f.lower() or "pages/" in f.replace("\\", "/")]
|
|
30
|
+
|
|
31
|
+
existing_sample = ""
|
|
32
|
+
if existing:
|
|
33
|
+
try:
|
|
34
|
+
sample = Path(existing[0]).read_text(encoding="utf-8")[:1000]
|
|
35
|
+
existing_sample = f"\n## Existing Test Pattern (from {existing[0]})\n```ts\n{sample}\n```\n"
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
targets = pages or staged
|
|
40
|
+
target_list = "\n".join(f"- {f}" for f in targets)
|
|
41
|
+
|
|
42
|
+
return f"""Generate Playwright E2E tests for the following pages/components.
|
|
43
|
+
|
|
44
|
+
## Files to Test
|
|
45
|
+
{target_list}
|
|
46
|
+
{existing_sample}
|
|
47
|
+
## Instructions
|
|
48
|
+
- Use TypeScript + `@playwright/test`
|
|
49
|
+
- File naming: `tests/{{page-name}}.spec.ts`
|
|
50
|
+
- Use the Page Object Model pattern — create a class per page
|
|
51
|
+
- Cover:
|
|
52
|
+
1. Happy path — the main user flow works end to end
|
|
53
|
+
2. Form validation — required fields, error messages shown correctly
|
|
54
|
+
3. Navigation — correct URL after actions
|
|
55
|
+
4. Error states — API failure, empty state, 404
|
|
56
|
+
- Assertions: `expect(locator).toBeVisible()`, `.toHaveText()`, `.toHaveURL()`
|
|
57
|
+
- Use `page.getByRole()`, `page.getByLabel()`, `page.getByTestId()` — not CSS selectors
|
|
58
|
+
- Run with: `npx playwright test`
|
|
59
|
+
|
|
60
|
+
## Example Page Object Pattern
|
|
61
|
+
```ts
|
|
62
|
+
import {{ Page, expect }} from '@playwright/test';
|
|
63
|
+
|
|
64
|
+
export class LoginPage {{
|
|
65
|
+
constructor(private page: Page) {{}}
|
|
66
|
+
|
|
67
|
+
async goto() {{ await this.page.goto('/login'); }}
|
|
68
|
+
async login(email: string, password: string) {{
|
|
69
|
+
await this.page.getByLabel('Email').fill(email);
|
|
70
|
+
await this.page.getByLabel('Password').fill(password);
|
|
71
|
+
await this.page.getByRole('button', {{ name: 'Sign in' }}).click();
|
|
72
|
+
}}
|
|
73
|
+
async expectError(message: string) {{
|
|
74
|
+
await expect(this.page.getByRole('alert')).toHaveText(message);
|
|
75
|
+
}}
|
|
76
|
+
}}
|
|
77
|
+
```
|
|
78
|
+
"""
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
SRC_EXTS = (".js", ".jsx", ".ts", ".tsx", ".vue")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_staged_files():
|
|
10
|
+
r = subprocess.run(["git", "diff", "--cached", "--name-only"],
|
|
11
|
+
capture_output=True, text=True)
|
|
12
|
+
return [
|
|
13
|
+
f.strip() for f in r.stdout.splitlines()
|
|
14
|
+
if f.strip().endswith(SRC_EXTS)
|
|
15
|
+
and not any(x in f for x in (".test.", ".spec.", "__tests__"))
|
|
16
|
+
and os.path.exists(f.strip())
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def infer_framework(src: str) -> str:
|
|
21
|
+
if "from 'react'" in src or 'from "react"' in src:
|
|
22
|
+
return "react"
|
|
23
|
+
if "from 'vue'" in src or 'from "vue"' in src:
|
|
24
|
+
return "vue"
|
|
25
|
+
if "@angular/core" in src:
|
|
26
|
+
return "angular"
|
|
27
|
+
return "vanilla"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def extract_exports(src: str) -> list:
|
|
31
|
+
exports = []
|
|
32
|
+
for m in re.finditer(r'export\s+(?:default\s+)?(?:async\s+)?(?:function|const|class)\s+(\w+)', src):
|
|
33
|
+
exports.append(m.group(1))
|
|
34
|
+
return list(dict.fromkeys(exports)) # deduplicate preserving order
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def extract_props(src: str) -> list:
|
|
38
|
+
m = re.search(r'(?:interface|type)\s+\w*Props\s*[={]([^}]+)}', src, re.DOTALL)
|
|
39
|
+
if not m:
|
|
40
|
+
return []
|
|
41
|
+
return re.findall(r'(\w+)\s*[?:]', m.group(1))[:6]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_generator() -> str:
|
|
45
|
+
staged = get_staged_files()
|
|
46
|
+
if not staged:
|
|
47
|
+
return "No staged source files found. Stage your files with `git add` first."
|
|
48
|
+
|
|
49
|
+
output_parts = []
|
|
50
|
+
|
|
51
|
+
for f in staged:
|
|
52
|
+
src = Path(f).read_text(encoding="utf-8")
|
|
53
|
+
framework = infer_framework(src)
|
|
54
|
+
exports = extract_exports(src)
|
|
55
|
+
props = extract_props(src)
|
|
56
|
+
fname = os.path.basename(f)
|
|
57
|
+
name = fname.rsplit(".", 1)[0]
|
|
58
|
+
test_file = f.rsplit(".", 1)[0] + ".test." + ("tsx" if f.endswith(".tsx") else "ts" if f.endswith(".ts") else "jsx" if f.endswith(".jsx") else "js")
|
|
59
|
+
|
|
60
|
+
if framework == "react":
|
|
61
|
+
prompt = f"""Generate Jest + React Testing Library tests for `{fname}`.
|
|
62
|
+
|
|
63
|
+
File: {f}
|
|
64
|
+
Exports: {', '.join(exports) if exports else name}
|
|
65
|
+
Props: {', '.join(props) if props else 'none detected'}
|
|
66
|
+
|
|
67
|
+
## Source
|
|
68
|
+
```tsx
|
|
69
|
+
{src[:3000]}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Instructions
|
|
73
|
+
- Test file: `{test_file}`
|
|
74
|
+
- Import from: `@testing-library/react` + `@testing-library/user-event`
|
|
75
|
+
- Cover: renders correctly, prop variations, user interactions, error/loading states
|
|
76
|
+
- Method naming: `should {{expected}} when {{condition}}`
|
|
77
|
+
- Mock fetch/axios calls with `jest.fn()`
|
|
78
|
+
- Use `screen.getByRole`, `getByText`, `getByTestId` — no class/id selectors
|
|
79
|
+
- Run with: `npx jest {test_file} --coverage`
|
|
80
|
+
"""
|
|
81
|
+
elif framework == "vue":
|
|
82
|
+
prompt = f"""Generate Jest + Vue Test Utils tests for `{fname}`.
|
|
83
|
+
|
|
84
|
+
File: {f}
|
|
85
|
+
Exports: {', '.join(exports) if exports else name}
|
|
86
|
+
|
|
87
|
+
## Source
|
|
88
|
+
```vue
|
|
89
|
+
{src[:3000]}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Instructions
|
|
93
|
+
- Test file: `{test_file}`
|
|
94
|
+
- Import from: `@vue/test-utils`
|
|
95
|
+
- Use `mount` for full rendering, `shallowMount` for unit isolation
|
|
96
|
+
- Cover: props, emits, slots, computed values, user interactions
|
|
97
|
+
- Mock Pinia stores with `createTestingPinia()`
|
|
98
|
+
"""
|
|
99
|
+
else:
|
|
100
|
+
prompt = f"""Generate Jest tests for `{fname}`.
|
|
101
|
+
|
|
102
|
+
File: {f}
|
|
103
|
+
Exports: {', '.join(exports) if exports else name}
|
|
104
|
+
|
|
105
|
+
## Source
|
|
106
|
+
```ts
|
|
107
|
+
{src[:3000]}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Instructions
|
|
111
|
+
- Test file: `{test_file}`
|
|
112
|
+
- Cover: all exported functions — happy path, edge cases, exceptions
|
|
113
|
+
- Mock dependencies with `jest.fn()` and `jest.spyOn()`
|
|
114
|
+
- Method naming: `should {{expected}} when {{condition}}`
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
output_parts.append(prompt)
|
|
118
|
+
|
|
119
|
+
return "\n\n---\n\n".join(output_parts)
|